@effect-agent/pr-review 0.1.0-beta.22 → 0.1.0-beta.24
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 +72 -48
- package/dist/action.d.mts +13 -13
- package/dist/action.mjs +42 -25
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-n-00ppWr.d.mts → fan-out-Bi1v0VaU.d.mts} +314 -307
- package/dist/{github-BgtP7Rdv.mjs → github-C6jrBLA2.mjs} +763 -285
- package/dist/github-C6jrBLA2.mjs.map +1 -0
- package/dist/index.d.mts +84 -126
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BguZK4B_.mjs → providers-2Jao2ZAX.mjs} +173 -585
- package/dist/providers-2Jao2ZAX.mjs.map +1 -0
- package/dist/testing.d.mts +6 -15
- package/dist/testing.mjs +14 -42
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +58 -34
- package/src/internal/action-entry.ts +0 -1
- package/src/internal/coverage.ts +147 -548
- package/src/internal/factory.ts +33 -54
- package/src/internal/fan-out-scripted.ts +26 -80
- package/src/internal/fan-out.ts +669 -432
- package/src/internal/fingerprint.ts +16 -10
- package/src/internal/fixtures.ts +6 -0
- package/src/internal/github.ts +36 -7
- package/src/internal/profiles.ts +8 -8
- package/src/internal/render.ts +34 -45
- package/src/internal/retirement.ts +3 -1
- package/src/internal/review-state.ts +204 -89
- package/src/internal/review-units.ts +25 -0
- package/src/internal/run.ts +231 -174
- package/src/internal/source.ts +1 -1
- package/dist/github-BgtP7Rdv.mjs.map +0 -1
- package/dist/providers-BguZK4B_.mjs.map +0 -1
package/src/internal/coverage.ts
CHANGED
|
@@ -1,47 +1,24 @@
|
|
|
1
1
|
import { Option, Schema } from "effect";
|
|
2
2
|
import type { RunEvent } from "effect-agent";
|
|
3
3
|
|
|
4
|
-
import { anchorViolation } from "./anchors.ts";
|
|
5
4
|
import type { ChangedFile } from "./diff.ts";
|
|
6
5
|
import { isReviewableFile } from "./diff.ts";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
type CandidateAssessment,
|
|
10
|
-
confirmedFindingForPublication,
|
|
11
|
-
FileReviewDelegationFailure,
|
|
12
|
-
FileReviewRequest,
|
|
13
|
-
FileReviewUnitResult,
|
|
14
|
-
ReviewCandidate,
|
|
15
|
-
reviewCandidateSubjectKey,
|
|
16
|
-
} from "./fan-out.ts";
|
|
17
|
-
import {
|
|
18
|
-
FileDiffView,
|
|
19
|
-
FileDiffQuery,
|
|
20
|
-
type ReviewConcern,
|
|
21
|
-
type ReviewFinding,
|
|
22
|
-
type WalkthroughEntry,
|
|
23
|
-
} from "./review-agent.ts";
|
|
24
|
-
import {
|
|
25
|
-
findingAnchorInUnitEvidence,
|
|
26
|
-
planReviewUnits,
|
|
27
|
-
type ReviewDiscoveryPass,
|
|
28
|
-
type ReviewUnit,
|
|
29
|
-
} from "./review-units.ts";
|
|
6
|
+
import { FileDiffView, FileDiffQuery } from "./review-agent.ts";
|
|
7
|
+
import type { ReviewUnitPlan } from "./review-units.ts";
|
|
30
8
|
|
|
31
9
|
// ---------------------------------------------------------------------------
|
|
32
10
|
// Two different claims are deliberately modeled:
|
|
33
11
|
//
|
|
34
12
|
// - input coverage: every required path was assigned bounded evidence or was
|
|
35
13
|
// explicitly reported outside the pipeline's capacity;
|
|
36
|
-
// - review assurance: every
|
|
37
|
-
// candidate-verification
|
|
14
|
+
// - review assurance: every scheduled discovery/specialist pass and every
|
|
15
|
+
// candidate-verification pass settled.
|
|
38
16
|
//
|
|
39
|
-
// Neither claims that the model found every defect.
|
|
17
|
+
// Neither claims that the model found every defect. The fan-out pipeline is
|
|
18
|
+
// host-scheduled (fan-out.ts), so its assurance is computed from direct pass
|
|
19
|
+
// results; only the flat reviewer is assessed from its Run event trace here.
|
|
40
20
|
// ---------------------------------------------------------------------------
|
|
41
21
|
|
|
42
|
-
export const ReviewShape = Schema.Literals(["flat", "fan-out"]);
|
|
43
|
-
export type ReviewShape = typeof ReviewShape.Type;
|
|
44
|
-
|
|
45
22
|
export class FailedReviewUnit extends Schema.Class<FailedReviewUnit>(
|
|
46
23
|
"@effect-agent/pr-review/FailedReviewUnit",
|
|
47
24
|
)({
|
|
@@ -90,6 +67,16 @@ export class ReviewInputCoverage extends Schema.Class<ReviewInputCoverage>(
|
|
|
90
67
|
unassignedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
|
|
91
68
|
Schema.isMaxLength(300),
|
|
92
69
|
),
|
|
70
|
+
/**
|
|
71
|
+
* Paths with neither a textual diff nor bounded base/head text (binaries,
|
|
72
|
+
* oversized files). Fail-closed: they keep the status incomplete for as
|
|
73
|
+
* long as they are part of the pull request — an unreviewable change must
|
|
74
|
+
* never authorize a green check. Exclude them deliberately with ignore
|
|
75
|
+
* globs when that is intended.
|
|
76
|
+
*/
|
|
77
|
+
undiffablePaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
|
|
78
|
+
Schema.isMaxLength(300),
|
|
79
|
+
),
|
|
93
80
|
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1_000))).check(
|
|
94
81
|
Schema.isMaxLength(20),
|
|
95
82
|
),
|
|
@@ -103,6 +90,13 @@ export class FailedReviewPass extends Schema.Class<FailedReviewPass>(
|
|
|
103
90
|
errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
104
91
|
}) {}
|
|
105
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Settlement of scheduled review work. `incomplete` means reviewer-side work
|
|
95
|
+
* failed after its bounded retry — a machinery gap that is carried forward and
|
|
96
|
+
* retried on the next run, never a statement about the code under review.
|
|
97
|
+
* `unverified` is the flat reviewer's honest constant: one pass with no
|
|
98
|
+
* independent verifier is neither settled assurance nor a failure.
|
|
99
|
+
*/
|
|
106
100
|
export class ReviewAssurance extends Schema.Class<ReviewAssurance>(
|
|
107
101
|
"@effect-agent/pr-review/ReviewAssurance",
|
|
108
102
|
)({
|
|
@@ -117,7 +111,8 @@ export class ReviewAssurance extends Schema.Class<ReviewAssurance>(
|
|
|
117
111
|
confirmedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
118
112
|
rejectedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
119
113
|
unsettledCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
120
|
-
/**
|
|
114
|
+
/** Discovery claims discarded for anchors/paths outside their assigned evidence. */
|
|
115
|
+
discardedInvalidFindings: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
121
116
|
failedPasses: Schema.Array(FailedReviewPass).check(Schema.isMaxLength(64)),
|
|
122
117
|
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1_000))).check(
|
|
123
118
|
Schema.isMaxLength(32),
|
|
@@ -145,7 +140,8 @@ const toolTrace = (events: ReadonlyArray<RunEvent>): ToolTrace => {
|
|
|
145
140
|
const sortedUnique = (values: Iterable<string>): ReadonlyArray<string> =>
|
|
146
141
|
[...new Set(values)].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
147
142
|
|
|
148
|
-
|
|
143
|
+
/** Render a bounded, deterministic "label (n): a, b, … (+k more)" reason line. */
|
|
144
|
+
export const boundedListReason = (label: string, values: Iterable<string>): string => {
|
|
149
145
|
const items = sortedUnique(values);
|
|
150
146
|
const prefix = `${label} (${items.length}): `;
|
|
151
147
|
let rendered = prefix;
|
|
@@ -163,12 +159,64 @@ const boundedListReason = (label: string, values: Iterable<string>): string => {
|
|
|
163
159
|
return rendered;
|
|
164
160
|
};
|
|
165
161
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
): ReviewInputCoverage =>
|
|
171
|
-
|
|
162
|
+
const anchorSurfaceAdjusted = (
|
|
163
|
+
inputCoverage: ReviewInputCoverage,
|
|
164
|
+
anchorFiles: ReadonlyArray<ChangedFile>,
|
|
165
|
+
totalAnchorFiles: number,
|
|
166
|
+
): ReviewInputCoverage =>
|
|
167
|
+
anchorFiles.length >= totalAnchorFiles
|
|
168
|
+
? inputCoverage
|
|
169
|
+
: ReviewInputCoverage.make({
|
|
170
|
+
...inputCoverage,
|
|
171
|
+
status: "incomplete",
|
|
172
|
+
reasons: [
|
|
173
|
+
...inputCoverage.reasons,
|
|
174
|
+
`full pull-request anchor surface exposed ${anchorFiles.length} of ${totalAnchorFiles} required files`,
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/** The flat reviewer's honest constant assurance: one pass, no verifier. */
|
|
179
|
+
export const flatAssurance = (): ReviewAssurance =>
|
|
180
|
+
ReviewAssurance.make({
|
|
181
|
+
status: "unverified",
|
|
182
|
+
requiredGeneralDiscoveryPasses: 1,
|
|
183
|
+
completedGeneralDiscoveryPasses: 1,
|
|
184
|
+
requiredSpecialistPasses: 0,
|
|
185
|
+
completedSpecialistPasses: 0,
|
|
186
|
+
requiredVerificationPasses: 0,
|
|
187
|
+
completedVerificationPasses: 0,
|
|
188
|
+
discoveredCandidates: 0,
|
|
189
|
+
confirmedCandidates: 0,
|
|
190
|
+
rejectedCandidates: 0,
|
|
191
|
+
unsettledCandidates: 0,
|
|
192
|
+
discardedInvalidFindings: 0,
|
|
193
|
+
failedPasses: [],
|
|
194
|
+
reasons: [
|
|
195
|
+
"flat review has no independent candidate-verification pass; use the fan-out pipeline for a settled assurance result",
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export interface FlatReviewAssessment {
|
|
200
|
+
readonly inputCoverage: ReviewInputCoverage;
|
|
201
|
+
readonly assurance: ReviewAssurance;
|
|
202
|
+
/** Retryable evidence gaps (failed or missing diff reads), never undiffable paths. */
|
|
203
|
+
readonly unreviewedPaths: ReadonlyArray<string>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Assess one settled flat run from its Run event trace: which required paths
|
|
208
|
+
* received successful bounded diff evidence. This observes tool INPUT
|
|
209
|
+
* assignment only — the host cannot know which evidence the model weighed.
|
|
210
|
+
*/
|
|
211
|
+
export const assessFlatReview = (input: {
|
|
212
|
+
readonly files: ReadonlyArray<ChangedFile>;
|
|
213
|
+
readonly totalFiles: number;
|
|
214
|
+
readonly anchorFiles: ReadonlyArray<ChangedFile>;
|
|
215
|
+
readonly totalAnchorFiles: number;
|
|
216
|
+
readonly events: ReadonlyArray<RunEvent>;
|
|
217
|
+
}): FlatReviewAssessment => {
|
|
218
|
+
const trace = toolTrace(input.events);
|
|
219
|
+
const requiredPaths = sortedUnique(input.files.map((file) => file.path));
|
|
172
220
|
const assigned = new Set<string>();
|
|
173
221
|
const partial = new Set<string>();
|
|
174
222
|
const failedPaths = new Set<string>();
|
|
@@ -184,15 +232,19 @@ const flatInputCoverage = (
|
|
|
184
232
|
}
|
|
185
233
|
if (trace.failed.has(toolCallId)) failedPaths.add(query.value.path);
|
|
186
234
|
}
|
|
187
|
-
const undiffable =
|
|
235
|
+
const undiffable = new Set(
|
|
236
|
+
input.files.filter((file) => !isReviewableFile(file)).map((file) => file.path),
|
|
237
|
+
);
|
|
188
238
|
const unassigned = requiredPaths.filter(
|
|
189
|
-
(path) => !
|
|
239
|
+
(path) => !undiffable.has(path) && (!assigned.has(path) || failedPaths.has(path)),
|
|
190
240
|
);
|
|
191
241
|
const reasons: Array<string> = [];
|
|
192
|
-
if (files.length < totalFiles) {
|
|
193
|
-
reasons.push(
|
|
242
|
+
if (input.files.length < input.totalFiles) {
|
|
243
|
+
reasons.push(
|
|
244
|
+
`review range exposed ${input.files.length} of ${input.totalFiles} required files`,
|
|
245
|
+
);
|
|
194
246
|
}
|
|
195
|
-
if (undiffable.
|
|
247
|
+
if (undiffable.size > 0) {
|
|
196
248
|
reasons.push(
|
|
197
249
|
boundedListReason("required paths have no reviewable diff or bounded text", undiffable),
|
|
198
250
|
);
|
|
@@ -204,26 +256,50 @@ const flatInputCoverage = (
|
|
|
204
256
|
if (unassigned.length > 0) {
|
|
205
257
|
reasons.push(boundedListReason("required paths received no successful diff input", unassigned));
|
|
206
258
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
259
|
+
const inputCoverage = anchorSurfaceAdjusted(
|
|
260
|
+
ReviewInputCoverage.make({
|
|
261
|
+
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
262
|
+
requiredPaths,
|
|
263
|
+
assignedPaths: sortedUnique(assigned),
|
|
264
|
+
partialPaths: sortedUnique(partial),
|
|
265
|
+
unassignedPaths: sortedUnique(unassigned),
|
|
266
|
+
undiffablePaths: sortedUnique(undiffable),
|
|
267
|
+
reasons,
|
|
268
|
+
}),
|
|
269
|
+
input.anchorFiles,
|
|
270
|
+
input.totalAnchorFiles,
|
|
271
|
+
);
|
|
272
|
+
return {
|
|
273
|
+
inputCoverage,
|
|
274
|
+
assurance: flatAssurance(),
|
|
275
|
+
// Everything still unreviewed and still part of the pull request carries
|
|
276
|
+
// forward — undiffable paths included, so the check stays fail-closed
|
|
277
|
+
// even after they leave the incremental delta.
|
|
278
|
+
unreviewedPaths: sortedUnique([...unassigned, ...undiffable]),
|
|
279
|
+
};
|
|
215
280
|
};
|
|
216
281
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Input coverage of one host-scheduled fan-out plan: which required paths the
|
|
284
|
+
* bounded plan actually assigned complete evidence for. Capacity overflow and
|
|
285
|
+
* undiffable paths are both real gaps; the pipeline carries them so the check
|
|
286
|
+
* stays fail-closed until they are reviewed, removed, or explicitly ignored.
|
|
287
|
+
*/
|
|
288
|
+
export const fanOutInputCoverage = (input: {
|
|
289
|
+
readonly plan: ReviewUnitPlan;
|
|
290
|
+
readonly files: ReadonlyArray<ChangedFile>;
|
|
291
|
+
readonly totalFiles: number;
|
|
292
|
+
readonly anchorFiles: ReadonlyArray<ChangedFile>;
|
|
293
|
+
readonly totalAnchorFiles: number;
|
|
294
|
+
}): ReviewInputCoverage => {
|
|
295
|
+
const plan = input.plan;
|
|
222
296
|
const assignedPaths = sortedUnique(plan.units.flatMap((unit) => unit.paths));
|
|
223
|
-
const unassignedPaths = sortedUnique(
|
|
297
|
+
const unassignedPaths = sortedUnique(plan.unassignedPaths);
|
|
224
298
|
const reasons: Array<string> = [];
|
|
225
299
|
if (plan.truncated) {
|
|
226
|
-
reasons.push(
|
|
300
|
+
reasons.push(
|
|
301
|
+
`review range exposed ${input.files.length} of ${input.totalFiles} required files`,
|
|
302
|
+
);
|
|
227
303
|
}
|
|
228
304
|
if (plan.undiffablePaths.length > 0) {
|
|
229
305
|
reasons.push(
|
|
@@ -255,419 +331,23 @@ const fanOutInputCoverage = (
|
|
|
255
331
|
if (plan.unassignedPaths.length > 0) {
|
|
256
332
|
reasons.push(boundedListReason("fan-out capacity left paths unassigned", plan.unassignedPaths));
|
|
257
333
|
}
|
|
258
|
-
return
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const sameStrings = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean =>
|
|
269
|
-
left.length === right.length && left.every((value, index) => value === right[index]);
|
|
270
|
-
|
|
271
|
-
const candidateKey = (candidate: ReviewCandidate): string =>
|
|
272
|
-
JSON.stringify(Schema.encodeSync(ReviewCandidate)(candidate));
|
|
273
|
-
|
|
274
|
-
const sameCandidates = (
|
|
275
|
-
left: ReadonlyArray<ReviewCandidate>,
|
|
276
|
-
right: ReadonlyArray<ReviewCandidate>,
|
|
277
|
-
): boolean =>
|
|
278
|
-
left.length === right.length &&
|
|
279
|
-
left.every((candidate, index) => {
|
|
280
|
-
const corresponding = right[index];
|
|
281
|
-
return corresponding !== undefined && candidateKey(candidate) === candidateKey(corresponding);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const delegationDeclarations = (trace: ToolTrace) =>
|
|
285
|
-
[...trace.declared].flatMap(([id, declaration]) => {
|
|
286
|
-
if (declaration.toolName !== "delegate_file_review") return [];
|
|
287
|
-
const request = Schema.decodeUnknownOption(FileReviewRequest)(declaration.parameters);
|
|
288
|
-
return Option.isNone(request) ? [] : [{ id, request: request.value }];
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
const failureTag = (trace: ToolTrace, id: string): string | undefined => {
|
|
292
|
-
const failed = trace.failed.get(id);
|
|
293
|
-
if (failed !== undefined) return failed.errorTag;
|
|
294
|
-
const succeeded = trace.succeeded.get(id);
|
|
295
|
-
if (succeeded === undefined) return undefined;
|
|
296
|
-
const returned = Schema.decodeUnknownOption(FileReviewDelegationFailure)(succeeded.result);
|
|
297
|
-
if (Option.isNone(returned)) return undefined;
|
|
298
|
-
return returned.value._tag === "FileReviewUnitFailed"
|
|
299
|
-
? `${returned.value._tag}:${returned.value.childErrorTag}`
|
|
300
|
-
: returned.value._tag;
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const exactDiscoveryRequest = (request: FileReviewRequest, pass: ReviewDiscoveryPass): boolean =>
|
|
304
|
-
request.phase === "discovery" &&
|
|
305
|
-
request.workId === pass.passId &&
|
|
306
|
-
request.unitId === pass.unitId &&
|
|
307
|
-
request.perspective === pass.perspective &&
|
|
308
|
-
sameStrings(request.paths, pass.paths) &&
|
|
309
|
-
sameStrings(request.evidenceShardIds, pass.evidenceShardIds) &&
|
|
310
|
-
sameStrings(request.riskCategories, pass.riskCategories) &&
|
|
311
|
-
request.candidates.length === 0;
|
|
312
|
-
|
|
313
|
-
const validCandidate = (
|
|
314
|
-
candidate: ReviewCandidate,
|
|
315
|
-
pass: ReviewDiscoveryPass,
|
|
316
|
-
unit: ReviewUnit,
|
|
317
|
-
files: ReadonlyArray<ChangedFile>,
|
|
318
|
-
anchorFiles: ReadonlyArray<ChangedFile>,
|
|
319
|
-
): boolean => {
|
|
320
|
-
const allowed = new Set(pass.paths);
|
|
321
|
-
const kind = candidate._tag === "FindingCandidate" ? "finding" : "concern";
|
|
322
|
-
const idPrefix = `${pass.passId}:${kind}:`;
|
|
323
|
-
return (
|
|
324
|
-
candidate.candidateId.startsWith(idPrefix) &&
|
|
325
|
-
/^\d{3}$/.test(candidate.candidateId.slice(idPrefix.length)) &&
|
|
326
|
-
candidate.workId === pass.passId &&
|
|
327
|
-
candidate.unitId === pass.unitId &&
|
|
328
|
-
candidate.evidencePaths.length > 0 &&
|
|
329
|
-
candidate.evidencePaths.every((path) => allowed.has(path)) &&
|
|
330
|
-
(candidate._tag !== "FindingCandidate" ||
|
|
331
|
-
(allowed.has(candidate.finding.path) &&
|
|
332
|
-
anchorViolation(candidate.finding, anchorFiles) === undefined &&
|
|
333
|
-
findingAnchorInUnitEvidence(candidate.finding, unit, files)))
|
|
334
|
-
);
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
interface AssuranceAssessment {
|
|
338
|
-
readonly assurance: ReviewAssurance;
|
|
339
|
-
readonly confirmedFindings: ReadonlyArray<ReviewFinding>;
|
|
340
|
-
readonly confirmedConcerns: ReadonlyArray<ReviewConcern>;
|
|
341
|
-
readonly walkthrough: ReadonlyArray<WalkthroughEntry>;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const flatAssurance = (): AssuranceAssessment => ({
|
|
345
|
-
assurance: ReviewAssurance.make({
|
|
346
|
-
status: "unverified",
|
|
347
|
-
requiredGeneralDiscoveryPasses: 1,
|
|
348
|
-
completedGeneralDiscoveryPasses: 1,
|
|
349
|
-
requiredSpecialistPasses: 0,
|
|
350
|
-
completedSpecialistPasses: 0,
|
|
351
|
-
requiredVerificationPasses: 1,
|
|
352
|
-
completedVerificationPasses: 0,
|
|
353
|
-
discoveredCandidates: 0,
|
|
354
|
-
confirmedCandidates: 0,
|
|
355
|
-
rejectedCandidates: 0,
|
|
356
|
-
unsettledCandidates: 0,
|
|
357
|
-
failedPasses: [],
|
|
358
|
-
reasons: [
|
|
359
|
-
"flat review has no independent candidate-verification pass; use the fan-out pipeline for a settled assurance result",
|
|
360
|
-
],
|
|
361
|
-
}),
|
|
362
|
-
confirmedFindings: [],
|
|
363
|
-
confirmedConcerns: [],
|
|
364
|
-
walkthrough: [],
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
const fanOutAssurance = (
|
|
368
|
-
files: ReadonlyArray<ChangedFile>,
|
|
369
|
-
totalFiles: number,
|
|
370
|
-
anchorFiles: ReadonlyArray<ChangedFile>,
|
|
371
|
-
trace: ToolTrace,
|
|
372
|
-
): AssuranceAssessment => {
|
|
373
|
-
const plan = planReviewUnits(files, { totalChangedFiles: totalFiles });
|
|
374
|
-
const declarations = delegationDeclarations(trace);
|
|
375
|
-
const consumedDeclarationIds = new Set<string>();
|
|
376
|
-
const failedPasses: Array<FailedReviewPass> = [];
|
|
377
|
-
const reasons: Array<string> = [];
|
|
378
|
-
const candidatesByUnit = new Map<string, Array<ReviewCandidate>>();
|
|
379
|
-
const walkthrough: Array<WalkthroughEntry> = [];
|
|
380
|
-
let completedGeneralDiscoveryPasses = 0;
|
|
381
|
-
let completedSpecialistPasses = 0;
|
|
382
|
-
|
|
383
|
-
for (const pass of plan.discoveryPasses) {
|
|
384
|
-
const unit = plan.units.find((candidate) => candidate.unitId === pass.unitId);
|
|
385
|
-
const matching = declarations.filter(({ request }) => exactDiscoveryRequest(request, pass));
|
|
386
|
-
const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
|
|
387
|
-
if (matching.length !== 1) {
|
|
388
|
-
failedPasses.push(
|
|
389
|
-
FailedReviewPass.make({
|
|
390
|
-
workId: pass.passId,
|
|
391
|
-
stage,
|
|
392
|
-
errorTag: matching.length === 0 ? "PassNotAssigned" : "PassAssignedMultipleTimes",
|
|
393
|
-
}),
|
|
394
|
-
);
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
const call = matching[0];
|
|
398
|
-
if (call === undefined) {
|
|
399
|
-
failedPasses.push(
|
|
400
|
-
FailedReviewPass.make({
|
|
401
|
-
workId: pass.passId,
|
|
402
|
-
stage,
|
|
403
|
-
errorTag: "PassLookupInvariantFailed",
|
|
404
|
-
}),
|
|
405
|
-
);
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
consumedDeclarationIds.add(call.id);
|
|
409
|
-
const failure = failureTag(trace, call.id);
|
|
410
|
-
const succeeded = trace.succeeded.get(call.id);
|
|
411
|
-
const result =
|
|
412
|
-
succeeded === undefined
|
|
413
|
-
? Option.none()
|
|
414
|
-
: Schema.decodeUnknownOption(FileReviewUnitResult)(succeeded.result);
|
|
415
|
-
if (
|
|
416
|
-
failure !== undefined ||
|
|
417
|
-
Option.isNone(result) ||
|
|
418
|
-
result.value.phase !== "discovery" ||
|
|
419
|
-
result.value.workId !== pass.passId ||
|
|
420
|
-
result.value.unitId !== pass.unitId ||
|
|
421
|
-
result.value.assessments.length !== 0
|
|
422
|
-
) {
|
|
423
|
-
failedPasses.push(
|
|
424
|
-
FailedReviewPass.make({
|
|
425
|
-
workId: pass.passId,
|
|
426
|
-
stage,
|
|
427
|
-
errorTag: failure ?? "DiscoveryDidNotSettleExactly",
|
|
428
|
-
}),
|
|
429
|
-
);
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
const ids = new Set<string>();
|
|
433
|
-
let candidatesValid = true;
|
|
434
|
-
for (const candidate of result.value.candidates) {
|
|
435
|
-
if (
|
|
436
|
-
unit === undefined ||
|
|
437
|
-
ids.has(candidate.candidateId) ||
|
|
438
|
-
!validCandidate(candidate, pass, unit, files, anchorFiles)
|
|
439
|
-
) {
|
|
440
|
-
candidatesValid = false;
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
ids.add(candidate.candidateId);
|
|
444
|
-
}
|
|
445
|
-
const unitCandidates = candidatesByUnit.get(pass.unitId) ?? [];
|
|
446
|
-
const unitIds = new Set(unitCandidates.map((candidate) => candidate.candidateId));
|
|
447
|
-
if (result.value.candidates.some((candidate) => unitIds.has(candidate.candidateId))) {
|
|
448
|
-
candidatesValid = false;
|
|
449
|
-
}
|
|
450
|
-
if (!candidatesValid) {
|
|
451
|
-
failedPasses.push(
|
|
452
|
-
FailedReviewPass.make({
|
|
453
|
-
workId: pass.passId,
|
|
454
|
-
stage,
|
|
455
|
-
errorTag: "DiscoveryCandidateMismatch",
|
|
456
|
-
}),
|
|
457
|
-
);
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if (stage === "specialist") {
|
|
461
|
-
completedSpecialistPasses += 1;
|
|
462
|
-
} else {
|
|
463
|
-
completedGeneralDiscoveryPasses += 1;
|
|
464
|
-
}
|
|
465
|
-
const subjectKeys = new Set(unitCandidates.map(reviewCandidateSubjectKey));
|
|
466
|
-
for (const candidate of result.value.candidates) {
|
|
467
|
-
const subjectKey = reviewCandidateSubjectKey(candidate);
|
|
468
|
-
if (subjectKeys.has(subjectKey)) continue;
|
|
469
|
-
subjectKeys.add(subjectKey);
|
|
470
|
-
unitCandidates.push(candidate);
|
|
471
|
-
}
|
|
472
|
-
candidatesByUnit.set(pass.unitId, unitCandidates);
|
|
473
|
-
if (pass.perspective === "general") {
|
|
474
|
-
const allowed = new Set(pass.paths);
|
|
475
|
-
walkthrough.push(...result.value.fileSummaries.filter((entry) => allowed.has(entry.path)));
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const confirmedCandidates: Array<{
|
|
480
|
-
readonly assessment: CandidateAssessment;
|
|
481
|
-
readonly candidate: ReviewCandidate;
|
|
482
|
-
}> = [];
|
|
483
|
-
let rejectedCandidates = 0;
|
|
484
|
-
let unsettledCandidates = 0;
|
|
485
|
-
let requiredVerificationPasses = 0;
|
|
486
|
-
let completedVerificationPasses = 0;
|
|
487
|
-
for (const unit of plan.units) {
|
|
488
|
-
const candidates = candidatesByUnit.get(unit.unitId) ?? [];
|
|
489
|
-
if (candidates.length === 0) continue;
|
|
490
|
-
requiredVerificationPasses += 1;
|
|
491
|
-
const workId = `${unit.unitId}-verification`;
|
|
492
|
-
const matching = declarations.filter(
|
|
493
|
-
({ request }) =>
|
|
494
|
-
request.phase === "verification" &&
|
|
495
|
-
request.workId === workId &&
|
|
496
|
-
request.unitId === unit.unitId &&
|
|
497
|
-
request.perspective === "candidate-verification" &&
|
|
498
|
-
sameStrings(request.paths, unit.paths) &&
|
|
499
|
-
sameStrings(
|
|
500
|
-
request.evidenceShardIds,
|
|
501
|
-
unit.evidenceShards.map((shard) => shard.shardId),
|
|
502
|
-
) &&
|
|
503
|
-
sameStrings(request.riskCategories, unit.riskCategories) &&
|
|
504
|
-
sameCandidates(request.candidates, candidates),
|
|
505
|
-
);
|
|
506
|
-
if (matching.length !== 1) {
|
|
507
|
-
unsettledCandidates += candidates.length;
|
|
508
|
-
failedPasses.push(
|
|
509
|
-
FailedReviewPass.make({
|
|
510
|
-
workId,
|
|
511
|
-
stage: "verification",
|
|
512
|
-
errorTag:
|
|
513
|
-
matching.length === 0
|
|
514
|
-
? "VerificationNotAssignedOrCandidateMismatch"
|
|
515
|
-
: "VerificationAssignedMultipleTimes",
|
|
516
|
-
}),
|
|
517
|
-
);
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
const call = matching[0];
|
|
521
|
-
if (call === undefined) {
|
|
522
|
-
unsettledCandidates += candidates.length;
|
|
523
|
-
failedPasses.push(
|
|
524
|
-
FailedReviewPass.make({
|
|
525
|
-
workId,
|
|
526
|
-
stage: "verification",
|
|
527
|
-
errorTag: "PassLookupInvariantFailed",
|
|
528
|
-
}),
|
|
529
|
-
);
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
consumedDeclarationIds.add(call.id);
|
|
533
|
-
const failure = failureTag(trace, call.id);
|
|
534
|
-
const succeeded = trace.succeeded.get(call.id);
|
|
535
|
-
const result =
|
|
536
|
-
succeeded === undefined
|
|
537
|
-
? Option.none()
|
|
538
|
-
: Schema.decodeUnknownOption(FileReviewUnitResult)(succeeded.result);
|
|
539
|
-
if (
|
|
540
|
-
failure !== undefined ||
|
|
541
|
-
Option.isNone(result) ||
|
|
542
|
-
result.value.phase !== "verification" ||
|
|
543
|
-
result.value.workId !== workId ||
|
|
544
|
-
result.value.unitId !== unit.unitId ||
|
|
545
|
-
result.value.candidates.length !== 0 ||
|
|
546
|
-
result.value.fileSummaries.length !== 0
|
|
547
|
-
) {
|
|
548
|
-
unsettledCandidates += candidates.length;
|
|
549
|
-
failedPasses.push(
|
|
550
|
-
FailedReviewPass.make({
|
|
551
|
-
workId,
|
|
552
|
-
stage: "verification",
|
|
553
|
-
errorTag: failure ?? "VerificationDidNotSettleExactly",
|
|
554
|
-
}),
|
|
555
|
-
);
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
const byId = new Map(
|
|
559
|
-
candidates.map((candidate) => [candidate.candidateId, candidate] as const),
|
|
560
|
-
);
|
|
561
|
-
const assessedIds = new Set<string>();
|
|
562
|
-
let suggestionSettlementExact = true;
|
|
563
|
-
const exactAssessments = result.value.assessments.every((assessment) => {
|
|
564
|
-
const candidate = byId.get(assessment.candidateId);
|
|
565
|
-
if (candidate === undefined || assessedIds.has(assessment.candidateId)) {
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
assessedIds.add(assessment.candidateId);
|
|
569
|
-
if (!assessmentSettlesSuggestionExactly(assessment, candidate)) {
|
|
570
|
-
suggestionSettlementExact = false;
|
|
571
|
-
}
|
|
572
|
-
return true;
|
|
573
|
-
});
|
|
574
|
-
if (
|
|
575
|
-
byId.size !== candidates.length ||
|
|
576
|
-
!exactAssessments ||
|
|
577
|
-
assessedIds.size !== byId.size ||
|
|
578
|
-
!suggestionSettlementExact
|
|
579
|
-
) {
|
|
580
|
-
unsettledCandidates += candidates.length;
|
|
581
|
-
failedPasses.push(
|
|
582
|
-
FailedReviewPass.make({
|
|
583
|
-
workId,
|
|
584
|
-
stage: "verification",
|
|
585
|
-
errorTag:
|
|
586
|
-
exactAssessments && !suggestionSettlementExact
|
|
587
|
-
? "SuggestionSettlementMismatch"
|
|
588
|
-
: "VerificationAssessmentMismatch",
|
|
589
|
-
}),
|
|
590
|
-
);
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
completedVerificationPasses += 1;
|
|
594
|
-
for (const assessment of result.value.assessments) {
|
|
595
|
-
if (assessment.disposition === "confirmed") {
|
|
596
|
-
const candidate = byId.get(assessment.candidateId);
|
|
597
|
-
if (candidate !== undefined) confirmedCandidates.push({ assessment, candidate });
|
|
598
|
-
} else {
|
|
599
|
-
rejectedCandidates += 1;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const unexpected = [...trace.declared].filter(
|
|
605
|
-
([id, declaration]) =>
|
|
606
|
-
declaration.toolName === "delegate_file_review" && !consumedDeclarationIds.has(id),
|
|
607
|
-
);
|
|
608
|
-
for (const [, declaration] of unexpected) {
|
|
609
|
-
const request = Schema.decodeUnknownOption(FileReviewRequest)(declaration.parameters);
|
|
610
|
-
failedPasses.push(
|
|
611
|
-
FailedReviewPass.make({
|
|
612
|
-
workId: Option.isSome(request) ? request.value.workId : "invalid-delegation-request",
|
|
613
|
-
stage:
|
|
614
|
-
Option.isSome(request) && request.value.phase === "verification"
|
|
615
|
-
? "verification"
|
|
616
|
-
: "discovery",
|
|
617
|
-
errorTag: "UnexpectedPass",
|
|
618
|
-
}),
|
|
619
|
-
);
|
|
620
|
-
}
|
|
621
|
-
if (failedPasses.length > 0) {
|
|
622
|
-
reasons.push(
|
|
623
|
-
boundedListReason(
|
|
624
|
-
"configured review passes did not settle",
|
|
625
|
-
failedPasses.map((pass) => `${pass.workId} (${pass.errorTag})`),
|
|
626
|
-
),
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
if (unsettledCandidates > 0) {
|
|
630
|
-
reasons.push(
|
|
631
|
-
`${unsettledCandidates} discovered candidate(s) did not receive exact verification`,
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
const requiredSpecialistPasses = plan.discoveryPasses.filter(
|
|
635
|
-
(pass) => pass.perspective === "risk-specialist",
|
|
636
|
-
).length;
|
|
637
|
-
const requiredGeneralDiscoveryPasses = plan.discoveryPasses.length - requiredSpecialistPasses;
|
|
638
|
-
const discoveredCandidates = [...candidatesByUnit.values()].reduce(
|
|
639
|
-
(total, candidates) => total + candidates.length,
|
|
640
|
-
0,
|
|
641
|
-
);
|
|
642
|
-
return {
|
|
643
|
-
assurance: ReviewAssurance.make({
|
|
644
|
-
status: reasons.length === 0 ? "settled" : "incomplete",
|
|
645
|
-
requiredGeneralDiscoveryPasses,
|
|
646
|
-
completedGeneralDiscoveryPasses,
|
|
647
|
-
requiredSpecialistPasses,
|
|
648
|
-
completedSpecialistPasses,
|
|
649
|
-
requiredVerificationPasses,
|
|
650
|
-
completedVerificationPasses,
|
|
651
|
-
discoveredCandidates,
|
|
652
|
-
confirmedCandidates: confirmedCandidates.length,
|
|
653
|
-
rejectedCandidates,
|
|
654
|
-
unsettledCandidates,
|
|
655
|
-
failedPasses,
|
|
334
|
+
return anchorSurfaceAdjusted(
|
|
335
|
+
ReviewInputCoverage.make({
|
|
336
|
+
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
337
|
+
requiredPaths: sortedUnique(input.files.map((file) => file.path)),
|
|
338
|
+
assignedPaths,
|
|
339
|
+
partialPaths: plan.partialEvidencePaths,
|
|
340
|
+
unassignedPaths,
|
|
341
|
+
undiffablePaths: sortedUnique(plan.undiffablePaths),
|
|
656
342
|
reasons,
|
|
657
343
|
}),
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
: [],
|
|
662
|
-
),
|
|
663
|
-
confirmedConcerns: confirmedCandidates.flatMap(({ candidate }) =>
|
|
664
|
-
candidate._tag === "ConcernCandidate" ? [candidate.concern] : [],
|
|
665
|
-
),
|
|
666
|
-
walkthrough,
|
|
667
|
-
};
|
|
344
|
+
input.anchorFiles,
|
|
345
|
+
input.totalAnchorFiles,
|
|
346
|
+
);
|
|
668
347
|
};
|
|
669
348
|
|
|
670
|
-
|
|
349
|
+
/** Compatibility aggregate over the two precise claims. */
|
|
350
|
+
export const compatibilityCoverage = (
|
|
671
351
|
inputCoverage: ReviewInputCoverage,
|
|
672
352
|
assurance: ReviewAssurance,
|
|
673
353
|
): ReviewCoverage => {
|
|
@@ -694,84 +374,3 @@ const compatibilityCoverage = (
|
|
|
694
374
|
reasons: [...inputCoverage.reasons, ...(assuranceIncomplete ? assurance.reasons : [])],
|
|
695
375
|
});
|
|
696
376
|
};
|
|
697
|
-
|
|
698
|
-
export interface ReviewPipelineAssessment {
|
|
699
|
-
readonly inputCoverage: ReviewInputCoverage;
|
|
700
|
-
readonly assurance: ReviewAssurance;
|
|
701
|
-
/** Deprecated compatibility aggregate. */
|
|
702
|
-
readonly coverage: ReviewCoverage;
|
|
703
|
-
readonly confirmedFindings: ReadonlyArray<ReviewFinding>;
|
|
704
|
-
readonly confirmedConcerns: ReadonlyArray<ReviewConcern>;
|
|
705
|
-
readonly walkthrough: ReadonlyArray<WalkthroughEntry>;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/** Assess one settled run without trusting coordinator prose or findings. */
|
|
709
|
-
export const assessReviewPipeline = (input: {
|
|
710
|
-
readonly shape: ReviewShape;
|
|
711
|
-
readonly files: ReadonlyArray<ChangedFile>;
|
|
712
|
-
readonly totalFiles: number;
|
|
713
|
-
readonly anchorFiles: ReadonlyArray<ChangedFile>;
|
|
714
|
-
readonly totalAnchorFiles: number;
|
|
715
|
-
readonly events: ReadonlyArray<RunEvent>;
|
|
716
|
-
}): ReviewPipelineAssessment => {
|
|
717
|
-
const trace = toolTrace(input.events);
|
|
718
|
-
let inputCoverage =
|
|
719
|
-
input.shape === "fan-out"
|
|
720
|
-
? fanOutInputCoverage(input.files, input.totalFiles)
|
|
721
|
-
: flatInputCoverage(input.files, input.totalFiles, trace);
|
|
722
|
-
if (input.anchorFiles.length < input.totalAnchorFiles) {
|
|
723
|
-
inputCoverage = ReviewInputCoverage.make({
|
|
724
|
-
...inputCoverage,
|
|
725
|
-
status: "incomplete",
|
|
726
|
-
reasons: [
|
|
727
|
-
...inputCoverage.reasons,
|
|
728
|
-
`full pull-request anchor surface exposed ${input.anchorFiles.length} of ${input.totalAnchorFiles} required files`,
|
|
729
|
-
],
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
const assessed =
|
|
733
|
-
input.shape === "fan-out"
|
|
734
|
-
? fanOutAssurance(input.files, input.totalFiles, input.anchorFiles, trace)
|
|
735
|
-
: flatAssurance();
|
|
736
|
-
return {
|
|
737
|
-
inputCoverage,
|
|
738
|
-
assurance: assessed.assurance,
|
|
739
|
-
coverage: compatibilityCoverage(inputCoverage, assessed.assurance),
|
|
740
|
-
confirmedFindings: assessed.confirmedFindings,
|
|
741
|
-
confirmedConcerns: assessed.confirmedConcerns,
|
|
742
|
-
walkthrough: assessed.walkthrough,
|
|
743
|
-
};
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
/** Compatibility helper; prefer assessReviewPipeline for precise claims. */
|
|
747
|
-
export const assessReviewCoverage = (input: {
|
|
748
|
-
readonly shape: ReviewShape;
|
|
749
|
-
readonly files: ReadonlyArray<ChangedFile>;
|
|
750
|
-
readonly totalFiles: number;
|
|
751
|
-
readonly anchorFiles: ReadonlyArray<ChangedFile>;
|
|
752
|
-
readonly totalAnchorFiles: number;
|
|
753
|
-
readonly events: ReadonlyArray<RunEvent>;
|
|
754
|
-
}): ReviewCoverage => assessReviewPipeline(input).coverage;
|
|
755
|
-
|
|
756
|
-
/** Host-verified summaries from successful general discovery passes only. */
|
|
757
|
-
export const collectUnitFileSummaries = (
|
|
758
|
-
events: ReadonlyArray<RunEvent>,
|
|
759
|
-
): ReadonlyArray<WalkthroughEntry> => {
|
|
760
|
-
const trace = toolTrace(events);
|
|
761
|
-
return delegationDeclarations(trace).flatMap(({ id, request }) => {
|
|
762
|
-
if (request.phase !== "discovery" || request.perspective !== "general") return [];
|
|
763
|
-
const success = trace.succeeded.get(id);
|
|
764
|
-
if (success === undefined || trace.failed.has(id)) return [];
|
|
765
|
-
const result = Schema.decodeUnknownOption(FileReviewUnitResult)(success.result);
|
|
766
|
-
if (
|
|
767
|
-
Option.isNone(result) ||
|
|
768
|
-
result.value.phase !== "discovery" ||
|
|
769
|
-
result.value.workId !== request.workId ||
|
|
770
|
-
result.value.unitId !== request.unitId
|
|
771
|
-
) {
|
|
772
|
-
return [];
|
|
773
|
-
}
|
|
774
|
-
const assigned = new Set(request.paths);
|
|
775
|
-
return result.value.fileSummaries.filter((entry) => assigned.has(entry.path));
|
|
776
|
-
});
|
|
777
|
-
};
|