@effect-agent/pr-review 0.1.0-beta.28 → 0.1.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -204
- package/dist/index.d.mts +92 -914
- package/dist/index.mjs +176 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -18
- package/src/index.ts +1 -25
- package/src/review.ts +244 -0
- package/dist/action.d.mts +0 -215
- package/dist/action.mjs +0 -505
- package/dist/action.mjs.map +0 -1
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -106
- package/dist/cli.mjs.map +0 -1
- package/dist/fan-out-C3yG1cx3.d.mts +0 -1526
- package/dist/github-CCuLgyqb.mjs +0 -3437
- package/dist/github-CCuLgyqb.mjs.map +0 -1
- package/dist/logging-Q4j0oub-.mjs +0 -75
- package/dist/logging-Q4j0oub-.mjs.map +0 -1
- package/dist/providers-Br9FRn7j.mjs +0 -1349
- package/dist/providers-Br9FRn7j.mjs.map +0 -1
- package/dist/testing.d.mts +0 -86
- package/dist/testing.mjs +0 -184
- package/dist/testing.mjs.map +0 -1
- package/src/action.ts +0 -906
- package/src/cli.ts +0 -235
- package/src/internal/action-entry.ts +0 -45
- package/src/internal/adjudication.ts +0 -415
- package/src/internal/anchors.ts +0 -20
- package/src/internal/coverage.ts +0 -357
- package/src/internal/diff.ts +0 -193
- package/src/internal/effort.ts +0 -86
- package/src/internal/factory.ts +0 -357
- package/src/internal/fan-out-scripted.ts +0 -77
- package/src/internal/fan-out.ts +0 -1148
- package/src/internal/fingerprint.ts +0 -89
- package/src/internal/fixtures.ts +0 -148
- package/src/internal/github-env.ts +0 -164
- package/src/internal/github.ts +0 -1218
- package/src/internal/ignore.ts +0 -88
- package/src/internal/logging.ts +0 -124
- package/src/internal/profiles.ts +0 -91
- package/src/internal/progress.ts +0 -433
- package/src/internal/providers.ts +0 -133
- package/src/internal/render.ts +0 -819
- package/src/internal/retirement.ts +0 -337
- package/src/internal/review-agent.ts +0 -543
- package/src/internal/review-state.ts +0 -782
- package/src/internal/review-units.ts +0 -493
- package/src/internal/run.ts +0 -611
- package/src/internal/scripted.ts +0 -108
- package/src/internal/source.ts +0 -110
- package/src/testing.ts +0 -8
package/src/internal/fan-out.ts
DELETED
|
@@ -1,1148 +0,0 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
Agent,
|
|
4
|
-
AgentPolicy,
|
|
5
|
-
AgentRuntime,
|
|
6
|
-
ToolResultBounds,
|
|
7
|
-
type BudgetAdapterError,
|
|
8
|
-
type BudgetExceeded,
|
|
9
|
-
type RunBudgetHook,
|
|
10
|
-
type RuntimeBinding,
|
|
11
|
-
} from "effect-agent";
|
|
12
|
-
import { Toolkit } from "effect/unstable/ai";
|
|
13
|
-
|
|
14
|
-
import type { PriorReviewContext } from "./adjudication.ts";
|
|
15
|
-
import { anchorViolation } from "./anchors.ts";
|
|
16
|
-
import { boundedListReason, FailedReviewPass, ReviewAssurance } from "./coverage.ts";
|
|
17
|
-
import { ChangedFileStatus, ChangedPath, type ChangedFile } from "./diff.ts";
|
|
18
|
-
import {
|
|
19
|
-
CodeReview,
|
|
20
|
-
fileReviewEvidenceChunks,
|
|
21
|
-
MAX_PATCH_CHARS,
|
|
22
|
-
MAX_WALKTHROUGH_SUMMARY_CHARS,
|
|
23
|
-
REVIEW_TOOL_RESULT_MAX_BYTES,
|
|
24
|
-
ReviewConcern,
|
|
25
|
-
ReviewFinding,
|
|
26
|
-
WalkthroughEntry,
|
|
27
|
-
} from "./review-agent.ts";
|
|
28
|
-
import {
|
|
29
|
-
MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS,
|
|
30
|
-
MAX_REVIEW_UNITS,
|
|
31
|
-
MAX_UNIT_EVIDENCE_SHARDS,
|
|
32
|
-
MAX_UNIT_FILES,
|
|
33
|
-
findingAnchorInUnitEvidence,
|
|
34
|
-
planReviewUnits,
|
|
35
|
-
rankAndDedupeConcerns,
|
|
36
|
-
rankAndDedupeFindings,
|
|
37
|
-
ReviewDiscoveryPass,
|
|
38
|
-
ReviewEvidenceShardId,
|
|
39
|
-
ReviewPassId,
|
|
40
|
-
ReviewRiskCategory,
|
|
41
|
-
ReviewUnit,
|
|
42
|
-
ReviewUnitId,
|
|
43
|
-
ReviewUnitPlan,
|
|
44
|
-
} from "./review-units.ts";
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// The assured fan-out reviewer is a bounded, deterministic three-stage
|
|
48
|
-
// pipeline scheduled ENTIRELY by host code:
|
|
49
|
-
//
|
|
50
|
-
// host plan -> independent discovery passes -> independent verification
|
|
51
|
-
//
|
|
52
|
-
// `planReviewUnits` is a pure function, so dispatch is plain Effect structured
|
|
53
|
-
// concurrency over its exact work list — there is no coordinator model, no
|
|
54
|
-
// delegation tool, and therefore no prompt-compliance failure mode. A pass
|
|
55
|
-
// that fails (child fault, malformed output) is retried once; a pass that
|
|
56
|
-
// still fails is recorded and its unit's paths are carried forward as
|
|
57
|
-
// retryable unreviewed scope instead of freezing the run's continuity
|
|
58
|
-
// baseline. A finding whose anchor is invalid is discarded and counted —
|
|
59
|
-
// never a reason to reject the whole pass.
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
/** One discovery pass returns at most this many anchored candidates. */
|
|
63
|
-
export const MAX_CHILD_FINDINGS = 6;
|
|
64
|
-
|
|
65
|
-
/** One discovery pass returns at most this many non-anchored candidates. */
|
|
66
|
-
export const MAX_CHILD_CONCERNS = 3;
|
|
67
|
-
|
|
68
|
-
/** Every unit receives independent general and specialist discovery passes. */
|
|
69
|
-
export const MAX_UNIT_CANDIDATES = (MAX_CHILD_FINDINGS + MAX_CHILD_CONCERNS) * 2;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* General + specialist discovery for every unit, then one verifier per unit.
|
|
73
|
-
* The one-retry budget doubles the worst-case child Run count, but the
|
|
74
|
-
* schedule itself never exceeds this bound.
|
|
75
|
-
*/
|
|
76
|
-
export const MAX_REVIEW_CHILDREN = MAX_REVIEW_UNITS * 3;
|
|
77
|
-
|
|
78
|
-
/** Bounded structured concurrency across units; passes inside a unit are sequential. */
|
|
79
|
-
export const REVIEW_UNIT_CONCURRENCY = 4;
|
|
80
|
-
|
|
81
|
-
/** Structural minimum for a child that exposes no tools. */
|
|
82
|
-
export const MAX_FILE_REVIEW_TOOL_CALLS = 1;
|
|
83
|
-
|
|
84
|
-
export const ReviewWorkPhase = Schema.Literals(["discovery", "verification"]);
|
|
85
|
-
export type ReviewWorkPhase = typeof ReviewWorkPhase.Type;
|
|
86
|
-
|
|
87
|
-
export const ReviewWorkPerspective = Schema.Literals([
|
|
88
|
-
"general",
|
|
89
|
-
"risk-specialist",
|
|
90
|
-
"candidate-verification",
|
|
91
|
-
]);
|
|
92
|
-
export type ReviewWorkPerspective = typeof ReviewWorkPerspective.Type;
|
|
93
|
-
|
|
94
|
-
export const ReviewCandidateId = Schema.NonEmptyString.check(Schema.isMaxLength(96));
|
|
95
|
-
|
|
96
|
-
export class FindingCandidate extends Schema.TaggedClass<FindingCandidate>()("FindingCandidate", {
|
|
97
|
-
candidateId: ReviewCandidateId,
|
|
98
|
-
workId: ReviewPassId,
|
|
99
|
-
unitId: ReviewUnitId,
|
|
100
|
-
finding: ReviewFinding,
|
|
101
|
-
evidencePaths: Schema.Array(ChangedPath)
|
|
102
|
-
.check(Schema.isMinLength(1))
|
|
103
|
-
.check(Schema.isMaxLength(1)),
|
|
104
|
-
}) {}
|
|
105
|
-
|
|
106
|
-
export class ConcernCandidate extends Schema.TaggedClass<ConcernCandidate>()("ConcernCandidate", {
|
|
107
|
-
candidateId: ReviewCandidateId,
|
|
108
|
-
workId: ReviewPassId,
|
|
109
|
-
unitId: ReviewUnitId,
|
|
110
|
-
concern: ReviewConcern,
|
|
111
|
-
evidencePaths: Schema.Array(ChangedPath)
|
|
112
|
-
.check(Schema.isMinLength(1))
|
|
113
|
-
.check(Schema.isMaxLength(3)),
|
|
114
|
-
}) {}
|
|
115
|
-
|
|
116
|
-
export const ReviewCandidate = Schema.Union([FindingCandidate, ConcernCandidate]);
|
|
117
|
-
export type ReviewCandidate = typeof ReviewCandidate.Type;
|
|
118
|
-
|
|
119
|
-
/** Deterministic host equivalence for claims repeated across discovery passes. */
|
|
120
|
-
export const reviewCandidateSubjectKey = (candidate: ReviewCandidate): string =>
|
|
121
|
-
candidate._tag === "FindingCandidate"
|
|
122
|
-
? `finding:${JSON.stringify(Schema.encodeSync(ReviewFinding)(candidate.finding))}`
|
|
123
|
-
: `concern:${JSON.stringify(Schema.encodeSync(ReviewConcern)(candidate.concern))}`;
|
|
124
|
-
|
|
125
|
-
export class CandidateAssessment extends Schema.Class<CandidateAssessment>(
|
|
126
|
-
"@effect-agent/pr-review/CandidateAssessment",
|
|
127
|
-
)({
|
|
128
|
-
candidateId: ReviewCandidateId,
|
|
129
|
-
disposition: Schema.Literals(["confirmed", "rejected"]),
|
|
130
|
-
/**
|
|
131
|
-
* Exact suggestion settlement: required when the candidate finding carries
|
|
132
|
-
* a suggestion, forbidden otherwise. Untrusted child output cannot publish
|
|
133
|
-
* a GitHub replacement block by prompt compliance alone — the host keeps a
|
|
134
|
-
* confirmed finding's suggestion only on an exact "committable" settlement.
|
|
135
|
-
*/
|
|
136
|
-
suggestion: Schema.optionalKey(
|
|
137
|
-
Schema.Literals(["committable", "not-committable"]).annotate({
|
|
138
|
-
description:
|
|
139
|
-
'Required exactly when the candidate finding carries a suggestion: "committable" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else. Forbidden for candidates without a suggestion.',
|
|
140
|
-
}),
|
|
141
|
-
),
|
|
142
|
-
rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
|
|
143
|
-
}) {}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Exact suggestion settlement shape: a carried suggestion must be settled and
|
|
147
|
-
* nothing else may be. A verification report that violates it is treated as a
|
|
148
|
-
* misbehaving pass and retried within the pass budget.
|
|
149
|
-
*/
|
|
150
|
-
export const assessmentSettlesSuggestionExactly = (
|
|
151
|
-
assessment: CandidateAssessment,
|
|
152
|
-
candidate: ReviewCandidate,
|
|
153
|
-
): boolean =>
|
|
154
|
-
candidate._tag === "FindingCandidate" && candidate.finding.suggestion !== undefined
|
|
155
|
-
? assessment.suggestion !== undefined
|
|
156
|
-
: assessment.suggestion === undefined;
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Fail-closed publication of a confirmed finding: only an exact "committable"
|
|
160
|
-
* settlement keeps the suggestion; anything else publishes the finding with
|
|
161
|
-
* the suggestion stripped so unverified text can never become a one-click
|
|
162
|
-
* GitHub replacement block.
|
|
163
|
-
*/
|
|
164
|
-
export const confirmedFindingForPublication = (
|
|
165
|
-
assessment: CandidateAssessment,
|
|
166
|
-
candidate: FindingCandidate,
|
|
167
|
-
): ReviewFinding => {
|
|
168
|
-
if (candidate.finding.suggestion === undefined || assessment.suggestion === "committable") {
|
|
169
|
-
return candidate.finding;
|
|
170
|
-
}
|
|
171
|
-
const { suggestion: _stripped, ...finding } = candidate.finding;
|
|
172
|
-
return ReviewFinding.make(finding);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Concern candidates need explicit paths internally to bind the claim to
|
|
177
|
-
* scheduled evidence. The verifier receives the complete bounded unit so it
|
|
178
|
-
* can use neighboring evidence to falsify the claim. The host copies these
|
|
179
|
-
* validated paths onto a confirmed public concern for incremental continuity.
|
|
180
|
-
*/
|
|
181
|
-
export class DiscoveredConcern extends Schema.Class<DiscoveredConcern>(
|
|
182
|
-
"@effect-agent/pr-review/DiscoveredConcern",
|
|
183
|
-
)({
|
|
184
|
-
concern: ReviewConcern,
|
|
185
|
-
evidencePaths: Schema.Array(ChangedPath)
|
|
186
|
-
.check(Schema.isMinLength(1))
|
|
187
|
-
.check(Schema.isMaxLength(3)),
|
|
188
|
-
}) {}
|
|
189
|
-
|
|
190
|
-
const UnitPaths = Schema.Array(ChangedPath)
|
|
191
|
-
.check(Schema.isMinLength(1))
|
|
192
|
-
.check(Schema.isMaxLength(MAX_UNIT_FILES));
|
|
193
|
-
|
|
194
|
-
/** Bounded prior-review context lines injected into discovery instructions. */
|
|
195
|
-
const UnitContextLines = Schema.Array(Schema.String.check(Schema.isMaxLength(1_200))).check(
|
|
196
|
-
Schema.isMaxLength(20),
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const RiskCategories = Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6));
|
|
200
|
-
const Candidates = Schema.Array(ReviewCandidate).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES));
|
|
201
|
-
const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId)
|
|
202
|
-
.check(Schema.isMinLength(1))
|
|
203
|
-
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS));
|
|
204
|
-
|
|
205
|
-
/** One complete host-selected evidence shard supplied to a review child. */
|
|
206
|
-
export class FileReviewEvidence extends Schema.Class<FileReviewEvidence>(
|
|
207
|
-
"@effect-agent/pr-review/FileReviewEvidence",
|
|
208
|
-
)({
|
|
209
|
-
shardId: ReviewEvidenceShardId,
|
|
210
|
-
path: ChangedPath,
|
|
211
|
-
status: ChangedFileStatus,
|
|
212
|
-
reviewMode: Schema.Literals(["diff", "content", "unavailable"]),
|
|
213
|
-
ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
214
|
-
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
215
|
-
annotatedPatch: Schema.String.check(Schema.isMaxLength(MAX_PATCH_CHARS)),
|
|
216
|
-
}) {}
|
|
217
|
-
|
|
218
|
-
/** Host-prepared child input with complete bounded diff/content evidence. */
|
|
219
|
-
export class FileReviewBrief extends Schema.Class<FileReviewBrief>(
|
|
220
|
-
"@effect-agent/pr-review/FileReviewBrief",
|
|
221
|
-
)({
|
|
222
|
-
phase: ReviewWorkPhase,
|
|
223
|
-
workId: ReviewPassId,
|
|
224
|
-
unitId: ReviewUnitId,
|
|
225
|
-
paths: UnitPaths,
|
|
226
|
-
evidenceShardIds: EvidenceShardIds,
|
|
227
|
-
perspective: ReviewWorkPerspective,
|
|
228
|
-
riskCategories: RiskCategories,
|
|
229
|
-
/** Empty for discovery; the exact discovered set for unit verification. */
|
|
230
|
-
candidates: Candidates,
|
|
231
|
-
evidence: Schema.Array(FileReviewEvidence)
|
|
232
|
-
.check(Schema.isMinLength(1))
|
|
233
|
-
.check(Schema.isMaxLength(MAX_UNIT_EVIDENCE_SHARDS)),
|
|
234
|
-
/** Maintainer-adjudicated identities on this unit; do not re-raise. */
|
|
235
|
-
adjudicatedContext: Schema.optionalKey(UnitContextLines),
|
|
236
|
-
/** Prior-round findings on this unit's re-reviewed paths. */
|
|
237
|
-
priorFindingContext: Schema.optionalKey(UnitContextLines),
|
|
238
|
-
}) {}
|
|
239
|
-
|
|
240
|
-
/** Child output; phase-inapplicable collections must be empty. */
|
|
241
|
-
export class FileReviewReport extends Schema.Class<FileReviewReport>(
|
|
242
|
-
"@effect-agent/pr-review/FileReviewReport",
|
|
243
|
-
)({
|
|
244
|
-
phase: ReviewWorkPhase,
|
|
245
|
-
workId: ReviewPassId,
|
|
246
|
-
unitId: ReviewUnitId,
|
|
247
|
-
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(MAX_CHILD_FINDINGS)),
|
|
248
|
-
concerns: Schema.Array(DiscoveredConcern).check(Schema.isMaxLength(MAX_CHILD_CONCERNS)),
|
|
249
|
-
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(MAX_UNIT_FILES)),
|
|
250
|
-
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(MAX_UNIT_CANDIDATES)),
|
|
251
|
-
}) {}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* A structurally valid child report that does not answer the scheduled pass:
|
|
255
|
-
* wrong identity, phase-inapplicable fields, or an inexact assessment set.
|
|
256
|
-
* Retried once like any other pass fault, because it is model misbehavior,
|
|
257
|
-
* not evidence about the code under review.
|
|
258
|
-
*/
|
|
259
|
-
export class ReviewPassMisbehaved extends Schema.TaggedError<ReviewPassMisbehaved>()(
|
|
260
|
-
"ReviewPassMisbehaved",
|
|
261
|
-
{
|
|
262
|
-
workId: ReviewPassId,
|
|
263
|
-
reason: Schema.NonEmptyString.check(Schema.isMaxLength(600)),
|
|
264
|
-
},
|
|
265
|
-
) {}
|
|
266
|
-
|
|
267
|
-
export interface FanOutInstructionOptions {
|
|
268
|
-
readonly guidance?: string | ReadonlyArray<string> | undefined;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const staticGuidanceLines = (
|
|
272
|
-
guidance: string | ReadonlyArray<string> | undefined,
|
|
273
|
-
): ReadonlyArray<string> => {
|
|
274
|
-
if (guidance === undefined) return [];
|
|
275
|
-
const lines = typeof guidance === "string" ? [guidance] : guidance;
|
|
276
|
-
return lines.filter((line) => line.length > 0);
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const evidenceInstructions = [
|
|
280
|
-
"The host placed complete bounded review evidence shards in the input evidence array. Treat every shard as required input; ordinal/total identifies multi-shard paths.",
|
|
281
|
-
"You have no tools and cannot roam outside this evidence. If it is insufficient for a candidate, reject or omit that candidate rather than guessing.",
|
|
282
|
-
"A diff marks new-version anchors as R<number>; only those lines may anchor findings. B/H content evidence is non-anchorable.",
|
|
283
|
-
];
|
|
284
|
-
|
|
285
|
-
/** Discovery and verification instructions share one child definition. */
|
|
286
|
-
export const makeFileReviewerInstructions =
|
|
287
|
-
(options: FanOutInstructionOptions = {}) =>
|
|
288
|
-
(brief: FileReviewBrief): string => {
|
|
289
|
-
const common = [
|
|
290
|
-
`You are an attached review worker for ${brief.workId} in host-planned unit ${brief.unitId}: ${brief.paths.join(", ")}.`,
|
|
291
|
-
...staticGuidanceLines(options.guidance),
|
|
292
|
-
...evidenceInstructions,
|
|
293
|
-
];
|
|
294
|
-
if (brief.phase === "verification") {
|
|
295
|
-
return [
|
|
296
|
-
...common,
|
|
297
|
-
"Independently verify every candidate in the input. You did not receive another reviewer's transcript or reasoning; use only the candidate claim and bounded evidence.",
|
|
298
|
-
"The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
|
|
299
|
-
"For each candidate, try to falsify it first. Confirm only when the cited behavior is supported and actionable. Reject unsupported, speculative, duplicate, or non-actionable candidates.",
|
|
300
|
-
'Return ONLY JSON with phase "verification", the exact workId/unitId, empty findings/concerns/fileSummaries arrays, and exactly one assessment per candidateId. Each assessment is {"candidateId": <exact id>, "disposition": <"confirmed" | "rejected">, "suggestion": <"committable" | "not-committable", present exactly when the candidate finding carries a suggestion>, "rationale": <bounded evidence-based reason>}. Never add or omit an id.',
|
|
301
|
-
'Settle every carried suggestion independently of the claim: answer "committable" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else — it compiles in context and preserves the finding\'s intent, never prose describing a change. Otherwise answer "not-committable"; the host then publishes the confirmed finding without its suggestion. Omit the assessment "suggestion" field for candidates without one.',
|
|
302
|
-
].join("\n");
|
|
303
|
-
}
|
|
304
|
-
const focus =
|
|
305
|
-
brief.perspective === "risk-specialist"
|
|
306
|
-
? brief.riskCategories.length > 0
|
|
307
|
-
? `This is a fresh specialist discovery pass. Concentrate on these host-classified risks without relying on another pass: ${brief.riskCategories.join(", ")}.`
|
|
308
|
-
: "This is a fresh specialist discovery pass. The host found no keyword-classified category, so independently scrutinize authentication/authorization, security boundaries, durability, concurrency, credentials, and external side effects rather than treating classification silence as low risk."
|
|
309
|
-
: "This is the general discovery pass. Review broadly for correctness, security, concurrency, resource, API, and error-handling defects.";
|
|
310
|
-
const adjudicated = brief.adjudicatedContext ?? [];
|
|
311
|
-
const priorFindings = brief.priorFindingContext ?? [];
|
|
312
|
-
return [
|
|
313
|
-
...common,
|
|
314
|
-
focus,
|
|
315
|
-
...(adjudicated.length === 0
|
|
316
|
-
? []
|
|
317
|
-
: [
|
|
318
|
-
"A maintainer has adjudicated these previously raised items on this unit (disposition, reason). Do not re-raise them unless you have materially new evidence, and if you do, say explicitly what changed since the adjudication:",
|
|
319
|
-
...adjudicated.map((line) => `- ${line}`),
|
|
320
|
-
]),
|
|
321
|
-
...(priorFindings.length === 0
|
|
322
|
-
? []
|
|
323
|
-
: [
|
|
324
|
-
"A previous review round raised these findings on this unit's paths. For each, either confirm it still holds, state that it is fixed, or withdraw it; do not demand the opposite of that prior guidance without explicitly acknowledging the reversal:",
|
|
325
|
-
...priorFindings.map((line) => `- ${line}`),
|
|
326
|
-
]),
|
|
327
|
-
"The discovery evidence array contains every complete shard in the unit. Review every entry and every shard of a multi-shard path. A later independent verifier, not you, decides which candidates publish.",
|
|
328
|
-
"Every non-anchored concern must list 1-3 exact evidencePaths to bind the claim to scheduled evidence. Report one root concern once; never split it into differently worded restatements.",
|
|
329
|
-
`Return ONLY JSON with phase "discovery", the exact workId/unitId, up to ${MAX_CHILD_FINDINGS} findings, up to ${MAX_CHILD_CONCERNS} concerns shaped as {concern, evidencePaths}, one factual file summary per path (<= ${MAX_WALKTHROUGH_SUMMARY_CHARS} chars), and an empty assessments array. Empty candidate arrays are valid; do not invent defects.`,
|
|
330
|
-
'Each finding is {"path": <a unit file path>, "startLine": <integer, an R-marked new-file line>, "endLine": <integer, >= startLine, same file, R-marked>, "severity": <"blocking" | "important" | "nit">, "category": <OPTIONAL problem-kind label>, "title": <string, <= 120 chars>, "body": <string, why it matters and what to do>, "suggestion": <string, OPTIONAL: replacement source code for exactly lines startLine..endLine, ready to commit>}.',
|
|
331
|
-
'Include "suggestion" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement source for every line in the range and nothing else — never prose describing the change, which belongs in "body".',
|
|
332
|
-
].join("\n");
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
export const fileReviewerInstructions = makeFileReviewerInstructions();
|
|
336
|
-
|
|
337
|
-
export const FileReviewToolkit = Toolkit.empty;
|
|
338
|
-
|
|
339
|
-
export const defaultFileReviewerPolicy = AgentPolicy.make({
|
|
340
|
-
maxTurns: 6,
|
|
341
|
-
maxToolCalls: MAX_FILE_REVIEW_TOOL_CALLS,
|
|
342
|
-
maxDuration: "6 minutes",
|
|
343
|
-
toolConcurrency: 2,
|
|
344
|
-
repeatedFailureLimit: 6,
|
|
345
|
-
tokenBudget: 200_000,
|
|
346
|
-
contextTokenLimit: 150_000,
|
|
347
|
-
toolResultBounds: ToolResultBounds.make({ maxBytes: REVIEW_TOOL_RESULT_MAX_BYTES }),
|
|
348
|
-
// Discovery or verification that exhausts is unsettled work, never a
|
|
349
|
-
// schema-valid partial that can contribute to a green assurance claim.
|
|
350
|
-
onExhaustion: "fail",
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
export const makeFileReviewerDefinition = (options: FanOutInstructionOptions = {}) =>
|
|
354
|
-
Agent.define("pr-review-worker", {
|
|
355
|
-
input: FileReviewBrief,
|
|
356
|
-
output: FileReviewReport,
|
|
357
|
-
instructions: makeFileReviewerInstructions(options),
|
|
358
|
-
toolkit: FileReviewToolkit,
|
|
359
|
-
policy: defaultFileReviewerPolicy,
|
|
360
|
-
description:
|
|
361
|
-
"Perform one bounded discovery or independent candidate-verification pass over host-supplied pull-request evidence.",
|
|
362
|
-
metadata: { deploymentClass: "E", surface: "read-only", stage: "discovery-verification" },
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
export const FileReviewer = makeFileReviewerDefinition();
|
|
366
|
-
|
|
367
|
-
/** The exact child binding shape the host pipeline schedules. */
|
|
368
|
-
export type FileReviewerBinding<Provider, ModelProvides, ModelRequires> = RuntimeBinding<
|
|
369
|
-
typeof FileReviewBrief,
|
|
370
|
-
typeof FileReviewReport,
|
|
371
|
-
ReturnType<typeof makeFileReviewerInstructions>,
|
|
372
|
-
Toolkit.Tools<typeof FileReviewToolkit>,
|
|
373
|
-
Provider,
|
|
374
|
-
ModelProvides,
|
|
375
|
-
ModelRequires
|
|
376
|
-
>;
|
|
377
|
-
|
|
378
|
-
// ---------------------------------------------------------------------------
|
|
379
|
-
// Host pipeline.
|
|
380
|
-
// ---------------------------------------------------------------------------
|
|
381
|
-
|
|
382
|
-
/** Everything one settled fan-out pipeline run produced, before publication. */
|
|
383
|
-
export interface FanOutPipelineOutcome {
|
|
384
|
-
readonly review: CodeReview;
|
|
385
|
-
readonly assurance: ReviewAssurance;
|
|
386
|
-
readonly plan: ReviewUnitPlan;
|
|
387
|
-
/** Paths of units with an unsettled pass — retryable scope for the next run. */
|
|
388
|
-
readonly unreviewedPaths: ReadonlyArray<string>;
|
|
389
|
-
/** Failed stages paired with the leftover paths they still own. */
|
|
390
|
-
readonly unreviewedPasses: ReadonlyArray<{
|
|
391
|
-
readonly stage: FailedReviewPass["stage"];
|
|
392
|
-
readonly paths: ReadonlyArray<string>;
|
|
393
|
-
}>;
|
|
394
|
-
/** Total settled child turns across every scheduled pass. */
|
|
395
|
-
readonly turns: number;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export interface FanOutPipelineInput {
|
|
399
|
-
readonly files: ReadonlyArray<ChangedFile>;
|
|
400
|
-
readonly anchorFiles: ReadonlyArray<ChangedFile>;
|
|
401
|
-
readonly totalChangedFiles: number;
|
|
402
|
-
readonly maxFindings?: number | undefined;
|
|
403
|
-
/** Shared run budget observed by every child pass. */
|
|
404
|
-
readonly budget?: RunBudgetHook<BudgetExceeded | BudgetAdapterError> | undefined;
|
|
405
|
-
/**
|
|
406
|
-
* Unchanged leftovers from prior failed passes. Every stage stays attached
|
|
407
|
-
* to its own paths; failed verification reopens both discovery perspectives
|
|
408
|
-
* for only those paths because candidate payloads are not persisted.
|
|
409
|
-
*/
|
|
410
|
-
readonly retry?:
|
|
411
|
-
| {
|
|
412
|
-
readonly passes?: ReadonlyArray<{
|
|
413
|
-
readonly stage: FailedReviewPass["stage"];
|
|
414
|
-
readonly paths: ReadonlyArray<string>;
|
|
415
|
-
}>;
|
|
416
|
-
/** @deprecated Pass path-bound `passes`; this flat form cannot preserve ownership. */
|
|
417
|
-
readonly paths?: ReadonlyArray<string>;
|
|
418
|
-
/** @deprecated Pass path-bound `passes`; this flat form cannot preserve ownership. */
|
|
419
|
-
readonly stages?: ReadonlyArray<FailedReviewPass["stage"]>;
|
|
420
|
-
}
|
|
421
|
-
| undefined;
|
|
422
|
-
/**
|
|
423
|
-
* Adjudicated identities and prior-round findings injected as discovery
|
|
424
|
-
* context on the units whose paths they touch. Context only — they never
|
|
425
|
-
* enter candidates or publication.
|
|
426
|
-
*/
|
|
427
|
-
readonly priorContext?: PriorReviewContext | undefined;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const candidateOrdinal = (index: number): string => String(index + 1).padStart(3, "0");
|
|
431
|
-
|
|
432
|
-
/** Rebuild one unit's complete evidence from the same snapshot the plan used. */
|
|
433
|
-
const unitEvidence = (
|
|
434
|
-
unit: ReviewUnit,
|
|
435
|
-
files: ReadonlyArray<ChangedFile>,
|
|
436
|
-
): Effect.Effect<ReadonlyArray<FileReviewEvidence>> =>
|
|
437
|
-
Effect.gen(function* () {
|
|
438
|
-
const byPath = new Map(files.map((file) => [file.path, file] as const));
|
|
439
|
-
const evidence: Array<FileReviewEvidence> = [];
|
|
440
|
-
for (const shard of unit.evidenceShards) {
|
|
441
|
-
const file = byPath.get(shard.path);
|
|
442
|
-
const chunk =
|
|
443
|
-
file === undefined ? undefined : fileReviewEvidenceChunks(file)[shard.ordinal - 1];
|
|
444
|
-
if (file === undefined || chunk === undefined) {
|
|
445
|
-
// The plan and this evidence derive from the same immutable snapshot
|
|
446
|
-
// via the same pure function; a mismatch is a host defect, not input.
|
|
447
|
-
return yield* Effect.die(
|
|
448
|
-
new Error(`planned evidence shard has no source: ${shard.shardId} (${shard.path})`),
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
evidence.push(
|
|
452
|
-
FileReviewEvidence.make({
|
|
453
|
-
shardId: shard.shardId,
|
|
454
|
-
path: shard.path,
|
|
455
|
-
status: file.status,
|
|
456
|
-
reviewMode: chunk.reviewMode,
|
|
457
|
-
ordinal: shard.ordinal,
|
|
458
|
-
total: shard.total,
|
|
459
|
-
annotatedPatch: chunk.annotatedPatch,
|
|
460
|
-
}),
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
return evidence;
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
interface SettledPass {
|
|
467
|
-
readonly report: FileReviewReport;
|
|
468
|
-
readonly turns: number;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
type PassOutcome =
|
|
472
|
-
| ({ readonly _tag: "settled" } & SettledPass)
|
|
473
|
-
| { readonly _tag: "failed"; readonly errorTag: string };
|
|
474
|
-
|
|
475
|
-
const misbehaved = (workId: string, reason: string) =>
|
|
476
|
-
ReviewPassMisbehaved.make({ workId, reason: reason.slice(0, 600) });
|
|
477
|
-
|
|
478
|
-
/** Validate that a verification report assesses exactly the scheduled candidates. */
|
|
479
|
-
const validateVerificationReport = (
|
|
480
|
-
brief: FileReviewBrief,
|
|
481
|
-
report: FileReviewReport,
|
|
482
|
-
): ReviewPassMisbehaved | undefined => {
|
|
483
|
-
if (report.findings.length > 0 || report.concerns.length > 0 || report.fileSummaries.length > 0) {
|
|
484
|
-
return misbehaved(brief.workId, "verification output contained discovery-only fields");
|
|
485
|
-
}
|
|
486
|
-
const expectedById = new Map(
|
|
487
|
-
brief.candidates.map((candidate) => [candidate.candidateId, candidate] as const),
|
|
488
|
-
);
|
|
489
|
-
const assessedIds = new Set<string>();
|
|
490
|
-
for (const assessment of report.assessments) {
|
|
491
|
-
const candidate = expectedById.get(assessment.candidateId);
|
|
492
|
-
if (candidate === undefined || assessedIds.has(assessment.candidateId)) {
|
|
493
|
-
return misbehaved(brief.workId, "verification output did not assess the exact candidate set");
|
|
494
|
-
}
|
|
495
|
-
if (!assessmentSettlesSuggestionExactly(assessment, candidate)) {
|
|
496
|
-
return misbehaved(
|
|
497
|
-
brief.workId,
|
|
498
|
-
"verification output did not settle suggestion publication exactly",
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
assessedIds.add(assessment.candidateId);
|
|
502
|
-
}
|
|
503
|
-
if (assessedIds.size !== expectedById.size) {
|
|
504
|
-
return misbehaved(brief.workId, "verification output did not assess the exact candidate set");
|
|
505
|
-
}
|
|
506
|
-
return undefined;
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Run one scheduled pass: execute the child, decode its report, and enforce
|
|
511
|
-
* the pass contract. Any typed fault — child failure, malformed or misdirected
|
|
512
|
-
* output — is retried once; budget exhaustion is terminal because a retry
|
|
513
|
-
* would fail the same way. The settled outcome is a value either way, so one
|
|
514
|
-
* flaky pass can never fail the whole pipeline.
|
|
515
|
-
*/
|
|
516
|
-
const runReviewPass = <Provider, ModelProvides, ModelRequires>(
|
|
517
|
-
binding: FileReviewerBinding<Provider, ModelProvides, ModelRequires>,
|
|
518
|
-
brief: FileReviewBrief,
|
|
519
|
-
budget: RunBudgetHook<BudgetExceeded | BudgetAdapterError> | undefined,
|
|
520
|
-
) =>
|
|
521
|
-
Effect.gen(function* () {
|
|
522
|
-
const result = yield* AgentRuntime.run(binding, brief, {
|
|
523
|
-
...(budget === undefined ? {} : { budget }),
|
|
524
|
-
estimateCostMicrousd: () => Effect.succeed(500),
|
|
525
|
-
});
|
|
526
|
-
const report = yield* Schema.decodeUnknownEffect(FileReviewReport)(result.output).pipe(
|
|
527
|
-
Effect.mapError((error) =>
|
|
528
|
-
misbehaved(brief.workId, `child report failed to decode: ${error.message}`),
|
|
529
|
-
),
|
|
530
|
-
);
|
|
531
|
-
if (
|
|
532
|
-
report.phase !== brief.phase ||
|
|
533
|
-
report.workId !== brief.workId ||
|
|
534
|
-
report.unitId !== brief.unitId
|
|
535
|
-
) {
|
|
536
|
-
return yield* misbehaved(
|
|
537
|
-
brief.workId,
|
|
538
|
-
"child report identity does not match the scheduled pass",
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
if (brief.phase === "verification") {
|
|
542
|
-
const violation = validateVerificationReport(brief, report);
|
|
543
|
-
if (violation !== undefined) return yield* violation;
|
|
544
|
-
} else if (report.assessments.length > 0) {
|
|
545
|
-
return yield* misbehaved(
|
|
546
|
-
brief.workId,
|
|
547
|
-
"discovery output contained verification-only assessments",
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
return { report, turns: result.turns } satisfies SettledPass;
|
|
551
|
-
}).pipe(
|
|
552
|
-
Effect.scoped,
|
|
553
|
-
Effect.retry({ times: 1, while: (error) => error._tag !== "BudgetExceeded" }),
|
|
554
|
-
Effect.map((settled): PassOutcome => ({ _tag: "settled", ...settled })),
|
|
555
|
-
Effect.catch((error) =>
|
|
556
|
-
Effect.succeed<PassOutcome>({ _tag: "failed", errorTag: String(error._tag).slice(0, 256) }),
|
|
557
|
-
),
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
interface DiscoveryHarvest {
|
|
561
|
-
readonly candidates: ReadonlyArray<ReviewCandidate>;
|
|
562
|
-
readonly fileSummaries: ReadonlyArray<WalkthroughEntry>;
|
|
563
|
-
readonly discarded: number;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Keep only findings anchored inside the pass's exact assigned evidence and
|
|
568
|
-
* concerns bound to unit paths. Everything else is discarded and counted —
|
|
569
|
-
* an invalid anchor invalidates one claim, never the pass that produced it.
|
|
570
|
-
*/
|
|
571
|
-
const harvestDiscovery = (
|
|
572
|
-
pass: ReviewDiscoveryPass,
|
|
573
|
-
unit: ReviewUnit,
|
|
574
|
-
files: ReadonlyArray<ChangedFile>,
|
|
575
|
-
anchorFiles: ReadonlyArray<ChangedFile>,
|
|
576
|
-
report: FileReviewReport,
|
|
577
|
-
): DiscoveryHarvest => {
|
|
578
|
-
const allowed = new Set(pass.paths);
|
|
579
|
-
let discarded = 0;
|
|
580
|
-
const keptFindings: Array<ReviewFinding> = [];
|
|
581
|
-
for (const finding of report.findings) {
|
|
582
|
-
if (
|
|
583
|
-
!allowed.has(finding.path) ||
|
|
584
|
-
anchorViolation(finding, anchorFiles) !== undefined ||
|
|
585
|
-
!findingAnchorInUnitEvidence(finding, unit, files)
|
|
586
|
-
) {
|
|
587
|
-
discarded += 1;
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
keptFindings.push(finding);
|
|
591
|
-
}
|
|
592
|
-
const keptConcerns: Array<DiscoveredConcern> = [];
|
|
593
|
-
for (const candidate of report.concerns) {
|
|
594
|
-
if (candidate.evidencePaths.some((path) => !allowed.has(path))) {
|
|
595
|
-
discarded += 1;
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
keptConcerns.push(candidate);
|
|
599
|
-
}
|
|
600
|
-
return {
|
|
601
|
-
candidates: [
|
|
602
|
-
...keptFindings.map((finding, index) =>
|
|
603
|
-
FindingCandidate.make({
|
|
604
|
-
candidateId: `${pass.passId}:finding:${candidateOrdinal(index)}`,
|
|
605
|
-
workId: pass.passId,
|
|
606
|
-
unitId: pass.unitId,
|
|
607
|
-
finding,
|
|
608
|
-
evidencePaths: [finding.path],
|
|
609
|
-
}),
|
|
610
|
-
),
|
|
611
|
-
...keptConcerns.map((candidate, index) =>
|
|
612
|
-
ConcernCandidate.make({
|
|
613
|
-
candidateId: `${pass.passId}:concern:${candidateOrdinal(index)}`,
|
|
614
|
-
workId: pass.passId,
|
|
615
|
-
unitId: pass.unitId,
|
|
616
|
-
concern: candidate.concern,
|
|
617
|
-
evidencePaths: candidate.evidencePaths,
|
|
618
|
-
}),
|
|
619
|
-
),
|
|
620
|
-
],
|
|
621
|
-
fileSummaries: report.fileSummaries.filter((entry) => allowed.has(entry.path)),
|
|
622
|
-
discarded,
|
|
623
|
-
};
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
interface UnitReviewOutcome {
|
|
627
|
-
readonly failedPasses: ReadonlyArray<FailedReviewPass>;
|
|
628
|
-
readonly discoveredCandidates: number;
|
|
629
|
-
readonly confirmed: ReadonlyArray<{
|
|
630
|
-
readonly assessment: CandidateAssessment;
|
|
631
|
-
readonly candidate: ReviewCandidate;
|
|
632
|
-
}>;
|
|
633
|
-
readonly rejectedCandidates: number;
|
|
634
|
-
readonly unsettledCandidates: number;
|
|
635
|
-
readonly discardedFindings: number;
|
|
636
|
-
readonly walkthrough: ReadonlyArray<WalkthroughEntry>;
|
|
637
|
-
readonly turns: number;
|
|
638
|
-
readonly completedGeneralPasses: number;
|
|
639
|
-
readonly completedSpecialistPasses: number;
|
|
640
|
-
readonly requiredVerificationPasses: number;
|
|
641
|
-
readonly completedVerificationPasses: number;
|
|
642
|
-
readonly unreviewedPaths: ReadonlyArray<string>;
|
|
643
|
-
readonly unreviewedPasses: ReadonlyArray<{
|
|
644
|
-
readonly stage: FailedReviewPass["stage"];
|
|
645
|
-
readonly paths: ReadonlyArray<string>;
|
|
646
|
-
}>;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const reviewUnit = <Provider, ModelProvides, ModelRequires>(
|
|
650
|
-
binding: FileReviewerBinding<Provider, ModelProvides, ModelRequires>,
|
|
651
|
-
unit: ReviewUnit,
|
|
652
|
-
passes: ReadonlyArray<ReviewDiscoveryPass>,
|
|
653
|
-
input: FanOutPipelineInput,
|
|
654
|
-
) =>
|
|
655
|
-
Effect.gen(function* () {
|
|
656
|
-
const evidence = yield* unitEvidence(unit, input.files);
|
|
657
|
-
const failedPasses: Array<FailedReviewPass> = [];
|
|
658
|
-
const candidates: Array<ReviewCandidate> = [];
|
|
659
|
-
const subjects = new Set<string>();
|
|
660
|
-
const walkthrough: Array<WalkthroughEntry> = [];
|
|
661
|
-
let discardedFindings = 0;
|
|
662
|
-
let turns = 0;
|
|
663
|
-
let completedGeneralPasses = 0;
|
|
664
|
-
let completedSpecialistPasses = 0;
|
|
665
|
-
// Discovery-only context: adjudicated identities (path-free entries apply
|
|
666
|
-
// to every unit) and prior-round findings on this unit's paths. The
|
|
667
|
-
// verifier stays unbiased — it judges only the candidate claims and the
|
|
668
|
-
// bounded evidence.
|
|
669
|
-
const unitPaths = new Set(unit.paths);
|
|
670
|
-
const adjudicatedContext = (input.priorContext?.adjudicated ?? [])
|
|
671
|
-
.filter((entry) => entry.path === undefined || unitPaths.has(entry.path))
|
|
672
|
-
.map((entry) => entry.line)
|
|
673
|
-
.slice(0, 20);
|
|
674
|
-
const priorFindingContext = (input.priorContext?.priorFindings ?? [])
|
|
675
|
-
.filter((entry) => unitPaths.has(entry.path))
|
|
676
|
-
.map((entry) => entry.line)
|
|
677
|
-
.slice(0, 20);
|
|
678
|
-
for (const pass of passes) {
|
|
679
|
-
const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
|
|
680
|
-
const brief = FileReviewBrief.make({
|
|
681
|
-
phase: "discovery",
|
|
682
|
-
workId: pass.passId,
|
|
683
|
-
unitId: pass.unitId,
|
|
684
|
-
paths: pass.paths,
|
|
685
|
-
evidenceShardIds: pass.evidenceShardIds,
|
|
686
|
-
perspective: pass.perspective,
|
|
687
|
-
riskCategories: pass.riskCategories,
|
|
688
|
-
candidates: [],
|
|
689
|
-
evidence,
|
|
690
|
-
...(adjudicatedContext.length === 0 ? {} : { adjudicatedContext }),
|
|
691
|
-
...(priorFindingContext.length === 0 ? {} : { priorFindingContext }),
|
|
692
|
-
});
|
|
693
|
-
const outcome = yield* runReviewPass(binding, brief, input.budget);
|
|
694
|
-
if (outcome._tag === "failed") {
|
|
695
|
-
failedPasses.push(
|
|
696
|
-
FailedReviewPass.make({ workId: pass.passId, stage, errorTag: outcome.errorTag }),
|
|
697
|
-
);
|
|
698
|
-
continue;
|
|
699
|
-
}
|
|
700
|
-
turns += outcome.turns;
|
|
701
|
-
if (stage === "specialist") {
|
|
702
|
-
completedSpecialistPasses += 1;
|
|
703
|
-
} else {
|
|
704
|
-
completedGeneralPasses += 1;
|
|
705
|
-
}
|
|
706
|
-
const harvest = harvestDiscovery(pass, unit, input.files, input.anchorFiles, outcome.report);
|
|
707
|
-
discardedFindings += harvest.discarded;
|
|
708
|
-
if (pass.perspective === "general") walkthrough.push(...harvest.fileSummaries);
|
|
709
|
-
for (const candidate of harvest.candidates) {
|
|
710
|
-
const subject = reviewCandidateSubjectKey(candidate);
|
|
711
|
-
if (subjects.has(subject)) continue;
|
|
712
|
-
subjects.add(subject);
|
|
713
|
-
candidates.push(candidate);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
const confirmed: Array<{
|
|
717
|
-
readonly assessment: CandidateAssessment;
|
|
718
|
-
readonly candidate: ReviewCandidate;
|
|
719
|
-
}> = [];
|
|
720
|
-
let rejectedCandidates = 0;
|
|
721
|
-
let unsettledCandidates = 0;
|
|
722
|
-
let completedVerificationPasses = 0;
|
|
723
|
-
const requiredVerificationPasses = candidates.length > 0 ? 1 : 0;
|
|
724
|
-
if (candidates.length > 0) {
|
|
725
|
-
const workId = `${unit.unitId}-verification`;
|
|
726
|
-
const brief = FileReviewBrief.make({
|
|
727
|
-
phase: "verification",
|
|
728
|
-
workId,
|
|
729
|
-
unitId: unit.unitId,
|
|
730
|
-
paths: unit.paths,
|
|
731
|
-
evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
|
|
732
|
-
perspective: "candidate-verification",
|
|
733
|
-
riskCategories: unit.riskCategories,
|
|
734
|
-
candidates,
|
|
735
|
-
evidence,
|
|
736
|
-
});
|
|
737
|
-
const outcome = yield* runReviewPass(binding, brief, input.budget);
|
|
738
|
-
if (outcome._tag === "failed") {
|
|
739
|
-
unsettledCandidates = candidates.length;
|
|
740
|
-
failedPasses.push(
|
|
741
|
-
FailedReviewPass.make({ workId, stage: "verification", errorTag: outcome.errorTag }),
|
|
742
|
-
);
|
|
743
|
-
} else {
|
|
744
|
-
turns += outcome.turns;
|
|
745
|
-
completedVerificationPasses = 1;
|
|
746
|
-
const byId = new Map(candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
747
|
-
for (const assessment of outcome.report.assessments) {
|
|
748
|
-
const candidate = byId.get(assessment.candidateId);
|
|
749
|
-
if (candidate === undefined) continue;
|
|
750
|
-
if (assessment.disposition === "confirmed") {
|
|
751
|
-
confirmed.push({ assessment, candidate });
|
|
752
|
-
} else {
|
|
753
|
-
rejectedCandidates += 1;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
return {
|
|
759
|
-
failedPasses,
|
|
760
|
-
discoveredCandidates: candidates.length,
|
|
761
|
-
confirmed,
|
|
762
|
-
rejectedCandidates,
|
|
763
|
-
unsettledCandidates,
|
|
764
|
-
discardedFindings,
|
|
765
|
-
walkthrough,
|
|
766
|
-
turns,
|
|
767
|
-
completedGeneralPasses,
|
|
768
|
-
completedSpecialistPasses,
|
|
769
|
-
requiredVerificationPasses,
|
|
770
|
-
completedVerificationPasses,
|
|
771
|
-
unreviewedPaths: failedPasses.length > 0 ? unit.paths : [],
|
|
772
|
-
unreviewedPasses: failedPasses.map((pass) => ({
|
|
773
|
-
stage: pass.stage,
|
|
774
|
-
paths: unit.paths,
|
|
775
|
-
})),
|
|
776
|
-
} satisfies UnitReviewOutcome;
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
const countNoun = (count: number, noun: string): string =>
|
|
780
|
-
`${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
781
|
-
|
|
782
|
-
const composeSummary = (plan: ReviewUnitPlan, assurance: ReviewAssurance): string => {
|
|
783
|
-
const requiredDiscovery =
|
|
784
|
-
assurance.requiredGeneralDiscoveryPasses + assurance.requiredSpecialistPasses;
|
|
785
|
-
const completedDiscovery =
|
|
786
|
-
assurance.completedGeneralDiscoveryPasses + assurance.completedSpecialistPasses;
|
|
787
|
-
const parts = [
|
|
788
|
-
`Reviewed ${countNoun(plan.totalFiles, "changed file")} across ${countNoun(plan.units.length, "bounded unit")}: ${completedDiscovery}/${requiredDiscovery} discovery and ${assurance.completedVerificationPasses}/${assurance.requiredVerificationPasses} verification pass(es) settled; ${assurance.confirmedCandidates} of ${countNoun(assurance.discoveredCandidates, "discovered candidate")} confirmed by independent verification.`,
|
|
789
|
-
];
|
|
790
|
-
if (assurance.failedPasses.length > 0) {
|
|
791
|
-
parts.push(
|
|
792
|
-
`${countNoun(assurance.failedPasses.length, "pass")} did not settle; the affected paths are carried forward and retried on the next run. This is a reviewer-side gap, not a code defect.`,
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
if (assurance.discardedInvalidFindings > 0) {
|
|
796
|
-
parts.push(
|
|
797
|
-
`${countNoun(assurance.discardedInvalidFindings, "candidate")} discarded for anchors or paths outside the assigned evidence.`,
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
if (plan.undiffablePaths.length > 0) {
|
|
801
|
-
parts.push(
|
|
802
|
-
`${countNoun(plan.undiffablePaths.length, "path")} had no reviewable textual evidence and keep input coverage incomplete; exclude such paths with ignore globs when that is intended.`,
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
if (plan.unassignedPaths.length > 0 || plan.unassignedEvidenceShardCount > 0) {
|
|
806
|
-
parts.push(
|
|
807
|
-
"The changeset exceeded the bounded fan-out capacity; unassigned scope is reported under input coverage.",
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
parts.push(
|
|
811
|
-
"No configured pipeline can prove absence of defects; this describes settled work only.",
|
|
812
|
-
);
|
|
813
|
-
return parts.join(" ").slice(0, 4_000);
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
const remapPlanUnitIds = (plan: ReviewUnitPlan, offset: number): ReviewUnitPlan => {
|
|
817
|
-
if (offset === 0) return plan;
|
|
818
|
-
const units = plan.units.map((unit, index) =>
|
|
819
|
-
ReviewUnit.make({
|
|
820
|
-
...unit,
|
|
821
|
-
unitId: `unit-${String(offset + index + 1).padStart(3, "0")}`,
|
|
822
|
-
}),
|
|
823
|
-
);
|
|
824
|
-
const mappedIds = new Map<string, string>();
|
|
825
|
-
for (const [index, unit] of plan.units.entries()) {
|
|
826
|
-
const remapped = units[index];
|
|
827
|
-
if (remapped !== undefined) {
|
|
828
|
-
mappedIds.set(unit.unitId, remapped.unitId);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return ReviewUnitPlan.make({
|
|
832
|
-
...plan,
|
|
833
|
-
units,
|
|
834
|
-
discoveryPasses: plan.discoveryPasses.map((pass) => {
|
|
835
|
-
const unitId = mappedIds.get(pass.unitId) ?? pass.unitId;
|
|
836
|
-
return ReviewDiscoveryPass.make({
|
|
837
|
-
...pass,
|
|
838
|
-
unitId,
|
|
839
|
-
passId: `${unitId}${pass.passId.slice(pass.unitId.length)}`,
|
|
840
|
-
});
|
|
841
|
-
}),
|
|
842
|
-
});
|
|
843
|
-
};
|
|
844
|
-
|
|
845
|
-
const scheduleFanOutWork = (
|
|
846
|
-
input: FanOutPipelineInput,
|
|
847
|
-
): {
|
|
848
|
-
readonly plan: ReviewUnitPlan;
|
|
849
|
-
readonly passesByUnit: Map<string, ReadonlyArray<ReviewDiscoveryPass>>;
|
|
850
|
-
readonly overflowRetryPasses: ReadonlyArray<{
|
|
851
|
-
readonly stage: FailedReviewPass["stage"];
|
|
852
|
-
readonly paths: ReadonlyArray<string>;
|
|
853
|
-
}>;
|
|
854
|
-
} => {
|
|
855
|
-
const requestedRetryPasses =
|
|
856
|
-
input.retry?.passes ??
|
|
857
|
-
input.retry?.stages?.map((stage) => ({ stage, paths: input.retry?.paths ?? [] })) ??
|
|
858
|
-
[];
|
|
859
|
-
const requestedStagesByPath = new Map<string, Set<FailedReviewPass["stage"]>>();
|
|
860
|
-
for (const pass of requestedRetryPasses) {
|
|
861
|
-
for (const path of pass.paths) {
|
|
862
|
-
const stages = requestedStagesByPath.get(path) ?? new Set<FailedReviewPass["stage"]>();
|
|
863
|
-
stages.add(pass.stage);
|
|
864
|
-
requestedStagesByPath.set(path, stages);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
const canonicalPathByKnownPath = new Map<string, string>();
|
|
868
|
-
const retryStagesByPath = new Map<string, Set<FailedReviewPass["stage"]>>();
|
|
869
|
-
for (const file of input.files) {
|
|
870
|
-
canonicalPathByKnownPath.set(file.path, file.path);
|
|
871
|
-
if (file.previousPath !== undefined) {
|
|
872
|
-
canonicalPathByKnownPath.set(file.previousPath, file.path);
|
|
873
|
-
}
|
|
874
|
-
const requested = [
|
|
875
|
-
requestedStagesByPath.get(file.path),
|
|
876
|
-
...(file.previousPath === undefined ? [] : [requestedStagesByPath.get(file.previousPath)]),
|
|
877
|
-
];
|
|
878
|
-
const stages = new Set(requested.flatMap((entry) => [...(entry ?? [])]));
|
|
879
|
-
if (stages.size > 0) retryStagesByPath.set(file.path, stages);
|
|
880
|
-
}
|
|
881
|
-
if (retryStagesByPath.size === 0) {
|
|
882
|
-
const plan = planReviewUnits(input.files, { totalChangedFiles: input.totalChangedFiles });
|
|
883
|
-
const passesByUnit = new Map<string, Array<ReviewDiscoveryPass>>();
|
|
884
|
-
for (const pass of plan.discoveryPasses) {
|
|
885
|
-
const passes = passesByUnit.get(pass.unitId) ?? [];
|
|
886
|
-
passes.push(pass);
|
|
887
|
-
passesByUnit.set(pass.unitId, passes);
|
|
888
|
-
}
|
|
889
|
-
return { plan, passesByUnit, overflowRetryPasses: [] };
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
type DiscoveryStage = "discovery" | "specialist";
|
|
893
|
-
const discoveryStagesFor = (
|
|
894
|
-
stages: ReadonlySet<FailedReviewPass["stage"]>,
|
|
895
|
-
): ReadonlyArray<DiscoveryStage> => {
|
|
896
|
-
if (stages.has("verification")) return ["discovery", "specialist"];
|
|
897
|
-
return [
|
|
898
|
-
...(stages.has("discovery") ? (["discovery"] as const) : []),
|
|
899
|
-
...(stages.has("specialist") ? (["specialist"] as const) : []),
|
|
900
|
-
];
|
|
901
|
-
};
|
|
902
|
-
const freshFiles = input.files.filter((file) => !retryStagesByPath.has(file.path));
|
|
903
|
-
const retryGroups = new Map<
|
|
904
|
-
string,
|
|
905
|
-
{ readonly stages: ReadonlyArray<DiscoveryStage>; readonly files: Array<ChangedFile> }
|
|
906
|
-
>();
|
|
907
|
-
for (const file of input.files) {
|
|
908
|
-
const retryStages = retryStagesByPath.get(file.path);
|
|
909
|
-
if (retryStages === undefined) continue;
|
|
910
|
-
const stages = discoveryStagesFor(retryStages);
|
|
911
|
-
const key = stages.join("|");
|
|
912
|
-
const group = retryGroups.get(key) ?? { stages, files: [] };
|
|
913
|
-
group.files.push(file);
|
|
914
|
-
retryGroups.set(key, group);
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
const batches: ReadonlyArray<{
|
|
918
|
-
readonly stages: ReadonlyArray<DiscoveryStage>;
|
|
919
|
-
readonly files: ReadonlyArray<ChangedFile>;
|
|
920
|
-
}> = [
|
|
921
|
-
...(freshFiles.length === 0
|
|
922
|
-
? []
|
|
923
|
-
: [{ stages: ["discovery", "specialist"] as const, files: freshFiles }]),
|
|
924
|
-
...[...retryGroups.entries()]
|
|
925
|
-
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
926
|
-
.map(([, group]) => group),
|
|
927
|
-
];
|
|
928
|
-
const subplans: Array<ReviewUnitPlan> = [];
|
|
929
|
-
const acceptedUnits: Array<ReviewUnit> = [];
|
|
930
|
-
const rejectedUnits: Array<ReviewUnit> = [];
|
|
931
|
-
const discoveryPasses: Array<ReviewDiscoveryPass> = [];
|
|
932
|
-
for (const batch of batches) {
|
|
933
|
-
const batchPlan = remapPlanUnitIds(
|
|
934
|
-
planReviewUnits(batch.files, { totalChangedFiles: batch.files.length }),
|
|
935
|
-
acceptedUnits.length,
|
|
936
|
-
);
|
|
937
|
-
subplans.push(batchPlan);
|
|
938
|
-
const accepted = batchPlan.units.slice(0, Math.max(0, MAX_REVIEW_UNITS - acceptedUnits.length));
|
|
939
|
-
acceptedUnits.push(...accepted);
|
|
940
|
-
rejectedUnits.push(...batchPlan.units.slice(accepted.length));
|
|
941
|
-
const acceptedIds = new Set(accepted.map((unit) => unit.unitId));
|
|
942
|
-
discoveryPasses.push(
|
|
943
|
-
...batchPlan.discoveryPasses.filter((pass) => {
|
|
944
|
-
const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
|
|
945
|
-
return acceptedIds.has(pass.unitId) && batch.stages.includes(stage);
|
|
946
|
-
}),
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const acceptedPaths = new Set(acceptedUnits.flatMap((unit) => unit.paths));
|
|
951
|
-
const incompletePlannedPaths = new Set([
|
|
952
|
-
...subplans.flatMap((plan) => plan.partialEvidencePaths),
|
|
953
|
-
...subplans.flatMap((plan) => plan.unassignedPaths),
|
|
954
|
-
...rejectedUnits.flatMap((unit) => unit.paths),
|
|
955
|
-
]);
|
|
956
|
-
const partialEvidencePaths = [...incompletePlannedPaths]
|
|
957
|
-
.filter((path) => acceptedPaths.has(path))
|
|
958
|
-
.sort();
|
|
959
|
-
const unassignedPaths = [...incompletePlannedPaths]
|
|
960
|
-
.filter((path) => !acceptedPaths.has(path))
|
|
961
|
-
.sort();
|
|
962
|
-
const rejectedEvidenceShards = rejectedUnits.flatMap((unit) => unit.evidenceShards);
|
|
963
|
-
const undiffablePaths = [...new Set(subplans.flatMap((plan) => plan.undiffablePaths))].sort();
|
|
964
|
-
const plan = ReviewUnitPlan.make({
|
|
965
|
-
totalFiles: input.files.length,
|
|
966
|
-
truncated: input.files.length < input.totalChangedFiles,
|
|
967
|
-
units: acceptedUnits,
|
|
968
|
-
discoveryPasses,
|
|
969
|
-
undiffablePaths,
|
|
970
|
-
partialEvidencePaths,
|
|
971
|
-
unassignedEvidenceShardCount:
|
|
972
|
-
subplans.reduce((total, item) => total + item.unassignedEvidenceShardCount, 0) +
|
|
973
|
-
rejectedEvidenceShards.length,
|
|
974
|
-
unassignedEvidenceShardIds: [
|
|
975
|
-
...subplans.flatMap((item) => item.unassignedEvidenceShardIds),
|
|
976
|
-
...rejectedEvidenceShards.map((shard) => shard.shardId),
|
|
977
|
-
].slice(0, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS),
|
|
978
|
-
unassignedPaths,
|
|
979
|
-
});
|
|
980
|
-
const passesByUnit = new Map<string, Array<ReviewDiscoveryPass>>();
|
|
981
|
-
for (const pass of discoveryPasses) {
|
|
982
|
-
const passes = passesByUnit.get(pass.unitId) ?? [];
|
|
983
|
-
passes.push(pass);
|
|
984
|
-
passesByUnit.set(pass.unitId, passes);
|
|
985
|
-
}
|
|
986
|
-
const incompletePaths = new Set([
|
|
987
|
-
...partialEvidencePaths,
|
|
988
|
-
...unassignedPaths,
|
|
989
|
-
...undiffablePaths,
|
|
990
|
-
]);
|
|
991
|
-
const overflowRetryPathsByStage = new Map<FailedReviewPass["stage"], Set<string>>();
|
|
992
|
-
for (const pass of requestedRetryPasses) {
|
|
993
|
-
for (const path of pass.paths) {
|
|
994
|
-
const canonicalPath = canonicalPathByKnownPath.get(path);
|
|
995
|
-
if (canonicalPath === undefined || !incompletePaths.has(canonicalPath)) continue;
|
|
996
|
-
const paths = overflowRetryPathsByStage.get(pass.stage) ?? new Set<string>();
|
|
997
|
-
paths.add(canonicalPath);
|
|
998
|
-
overflowRetryPathsByStage.set(pass.stage, paths);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
const overflowRetryPasses = (["discovery", "specialist", "verification"] as const).flatMap(
|
|
1002
|
-
(stage) => {
|
|
1003
|
-
const paths = [...(overflowRetryPathsByStage.get(stage) ?? [])].sort();
|
|
1004
|
-
return Array.from({ length: Math.ceil(paths.length / MAX_UNIT_FILES) }, (_, index) => ({
|
|
1005
|
-
stage,
|
|
1006
|
-
paths: paths.slice(index * MAX_UNIT_FILES, (index + 1) * MAX_UNIT_FILES),
|
|
1007
|
-
}));
|
|
1008
|
-
},
|
|
1009
|
-
);
|
|
1010
|
-
return { plan, passesByUnit, overflowRetryPasses };
|
|
1011
|
-
};
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* Run the complete host-scheduled fan-out pipeline over one selected
|
|
1015
|
-
* changeset snapshot: plan, independent discovery, exact verification, and a
|
|
1016
|
-
* deterministic host-composed CodeReview from verifier-confirmed candidates
|
|
1017
|
-
* only. The verdict is derived from confirmed severities, never model prose.
|
|
1018
|
-
*/
|
|
1019
|
-
export const runFanOutReview = <Provider, ModelProvides, ModelRequires>(
|
|
1020
|
-
binding: FileReviewerBinding<Provider, ModelProvides, ModelRequires>,
|
|
1021
|
-
input: FanOutPipelineInput,
|
|
1022
|
-
) =>
|
|
1023
|
-
Effect.gen(function* () {
|
|
1024
|
-
const { plan, passesByUnit, overflowRetryPasses } = scheduleFanOutWork(input);
|
|
1025
|
-
const outcomes = yield* Effect.forEach(
|
|
1026
|
-
plan.units,
|
|
1027
|
-
(unit) => reviewUnit(binding, unit, passesByUnit.get(unit.unitId) ?? [], input),
|
|
1028
|
-
{ concurrency: REVIEW_UNIT_CONCURRENCY },
|
|
1029
|
-
);
|
|
1030
|
-
const failedPasses = outcomes.flatMap((outcome) => outcome.failedPasses);
|
|
1031
|
-
const unsettledCandidates = outcomes.reduce(
|
|
1032
|
-
(total, outcome) => total + outcome.unsettledCandidates,
|
|
1033
|
-
0,
|
|
1034
|
-
);
|
|
1035
|
-
const reasons: Array<string> = [];
|
|
1036
|
-
if (failedPasses.length > 0) {
|
|
1037
|
-
reasons.push(
|
|
1038
|
-
boundedListReason(
|
|
1039
|
-
"configured review passes did not settle",
|
|
1040
|
-
failedPasses.map((pass) => `${pass.workId} (${pass.errorTag})`),
|
|
1041
|
-
),
|
|
1042
|
-
);
|
|
1043
|
-
}
|
|
1044
|
-
if (unsettledCandidates > 0) {
|
|
1045
|
-
reasons.push(
|
|
1046
|
-
`${unsettledCandidates} discovered candidate(s) did not receive exact verification`,
|
|
1047
|
-
);
|
|
1048
|
-
}
|
|
1049
|
-
const requiredSpecialistPasses = plan.discoveryPasses.filter(
|
|
1050
|
-
(pass) => pass.perspective === "risk-specialist",
|
|
1051
|
-
).length;
|
|
1052
|
-
const confirmed = outcomes.flatMap((outcome) => outcome.confirmed);
|
|
1053
|
-
const assurance = ReviewAssurance.make({
|
|
1054
|
-
status: reasons.length === 0 ? "settled" : "incomplete",
|
|
1055
|
-
requiredGeneralDiscoveryPasses: plan.discoveryPasses.length - requiredSpecialistPasses,
|
|
1056
|
-
completedGeneralDiscoveryPasses: outcomes.reduce(
|
|
1057
|
-
(total, outcome) => total + outcome.completedGeneralPasses,
|
|
1058
|
-
0,
|
|
1059
|
-
),
|
|
1060
|
-
requiredSpecialistPasses,
|
|
1061
|
-
completedSpecialistPasses: outcomes.reduce(
|
|
1062
|
-
(total, outcome) => total + outcome.completedSpecialistPasses,
|
|
1063
|
-
0,
|
|
1064
|
-
),
|
|
1065
|
-
requiredVerificationPasses: outcomes.reduce(
|
|
1066
|
-
(total, outcome) => total + outcome.requiredVerificationPasses,
|
|
1067
|
-
0,
|
|
1068
|
-
),
|
|
1069
|
-
completedVerificationPasses: outcomes.reduce(
|
|
1070
|
-
(total, outcome) => total + outcome.completedVerificationPasses,
|
|
1071
|
-
0,
|
|
1072
|
-
),
|
|
1073
|
-
discoveredCandidates: outcomes.reduce(
|
|
1074
|
-
(total, outcome) => total + outcome.discoveredCandidates,
|
|
1075
|
-
0,
|
|
1076
|
-
),
|
|
1077
|
-
confirmedCandidates: confirmed.length,
|
|
1078
|
-
rejectedCandidates: outcomes.reduce(
|
|
1079
|
-
(total, outcome) => total + outcome.rejectedCandidates,
|
|
1080
|
-
0,
|
|
1081
|
-
),
|
|
1082
|
-
unsettledCandidates,
|
|
1083
|
-
discardedInvalidFindings: outcomes.reduce(
|
|
1084
|
-
(total, outcome) => total + outcome.discardedFindings,
|
|
1085
|
-
0,
|
|
1086
|
-
),
|
|
1087
|
-
failedPasses,
|
|
1088
|
-
reasons,
|
|
1089
|
-
});
|
|
1090
|
-
const findings = rankAndDedupeFindings(
|
|
1091
|
-
confirmed.flatMap(({ assessment, candidate }) =>
|
|
1092
|
-
candidate._tag === "FindingCandidate"
|
|
1093
|
-
? [confirmedFindingForPublication(assessment, candidate)]
|
|
1094
|
-
: [],
|
|
1095
|
-
),
|
|
1096
|
-
);
|
|
1097
|
-
const concerns = rankAndDedupeConcerns(
|
|
1098
|
-
confirmed.flatMap(({ candidate }) =>
|
|
1099
|
-
candidate._tag === "ConcernCandidate"
|
|
1100
|
-
? [
|
|
1101
|
-
ReviewConcern.make({
|
|
1102
|
-
...candidate.concern,
|
|
1103
|
-
evidencePaths: [...new Set(candidate.evidencePaths)].sort(),
|
|
1104
|
-
}),
|
|
1105
|
-
]
|
|
1106
|
-
: [],
|
|
1107
|
-
),
|
|
1108
|
-
);
|
|
1109
|
-
const walkthrough = outcomes.flatMap((outcome) => outcome.walkthrough);
|
|
1110
|
-
const blocking =
|
|
1111
|
-
findings.some((finding) => finding.severity === "blocking") ||
|
|
1112
|
-
concerns.some((concern) => concern.severity === "blocking");
|
|
1113
|
-
const review = CodeReview.make({
|
|
1114
|
-
summary: composeSummary(plan, assurance),
|
|
1115
|
-
verdict: blocking
|
|
1116
|
-
? "request-changes"
|
|
1117
|
-
: findings.length > 0 || concerns.length > 0
|
|
1118
|
-
? "comment"
|
|
1119
|
-
: "approve",
|
|
1120
|
-
findings,
|
|
1121
|
-
...(concerns.length === 0 ? {} : { concerns }),
|
|
1122
|
-
...(walkthrough.length === 0 ? {} : { walkthrough }),
|
|
1123
|
-
});
|
|
1124
|
-
return {
|
|
1125
|
-
review,
|
|
1126
|
-
assurance,
|
|
1127
|
-
plan,
|
|
1128
|
-
// Everything not fully reviewed this run and still part of the pull
|
|
1129
|
-
// request carries forward, so the baseline can advance without ever
|
|
1130
|
-
// moving unreviewed scope behind a green check: failed units retry,
|
|
1131
|
-
// whole overflow files review in later installments, and partial or
|
|
1132
|
-
// undiffable files keep the check fail-closed until they are reviewed,
|
|
1133
|
-
// removed, or explicitly ignored.
|
|
1134
|
-
unreviewedPaths: [
|
|
1135
|
-
...new Set([
|
|
1136
|
-
...outcomes.flatMap((outcome) => outcome.unreviewedPaths),
|
|
1137
|
-
...plan.unassignedPaths,
|
|
1138
|
-
...plan.partialEvidencePaths,
|
|
1139
|
-
...plan.undiffablePaths,
|
|
1140
|
-
]),
|
|
1141
|
-
].sort(),
|
|
1142
|
-
unreviewedPasses: [
|
|
1143
|
-
...outcomes.flatMap((outcome) => outcome.unreviewedPasses),
|
|
1144
|
-
...overflowRetryPasses,
|
|
1145
|
-
],
|
|
1146
|
-
turns: outcomes.reduce((total, outcome) => total + outcome.turns, 0),
|
|
1147
|
-
} satisfies FanOutPipelineOutcome;
|
|
1148
|
-
});
|