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