@effect-agent/pr-review 0.1.0-beta.20 → 0.1.0-beta.22
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 +12 -6
- 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-n-00ppWr.d.mts} +308 -223
- package/dist/{github-DnenG3be.mjs → github-BgtP7Rdv.mjs} +652 -196
- package/dist/github-BgtP7Rdv.mjs.map +1 -0
- package/dist/index.d.mts +27 -54
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BE83_Tfo.mjs → providers-BguZK4B_.mjs} +408 -134
- package/dist/providers-BguZK4B_.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 +628 -134
- package/src/internal/factory.ts +12 -9
- package/src/internal/fan-out-scripted.ts +61 -94
- package/src/internal/fan-out.ts +560 -255
- 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 +74 -25
- 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,42 @@
|
|
|
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
|
+
assessmentSettlesSuggestionExactly,
|
|
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";
|
|
9
30
|
|
|
10
31
|
// ---------------------------------------------------------------------------
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
32
|
+
// Two different claims are deliberately modeled:
|
|
33
|
+
//
|
|
34
|
+
// - input coverage: every required path was assigned bounded evidence or was
|
|
35
|
+
// explicitly reported outside the pipeline's capacity;
|
|
36
|
+
// - review assurance: every configured discovery/specialist pass and every
|
|
37
|
+
// candidate-verification batch settled exactly.
|
|
38
|
+
//
|
|
39
|
+
// Neither claims that the model found every defect.
|
|
14
40
|
// ---------------------------------------------------------------------------
|
|
15
41
|
|
|
16
42
|
export const ReviewShape = Schema.Literals(["flat", "fan-out"]);
|
|
@@ -23,6 +49,11 @@ export class FailedReviewUnit extends Schema.Class<FailedReviewUnit>(
|
|
|
23
49
|
errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
24
50
|
}) {}
|
|
25
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Compatibility diagnostic retained for callers that consumed the original
|
|
54
|
+
* `coverage` field. New UI and state decisions use ReviewInputCoverage and
|
|
55
|
+
* ReviewAssurance directly.
|
|
56
|
+
*/
|
|
26
57
|
export class ReviewCoverage extends Schema.Class<ReviewCoverage>(
|
|
27
58
|
"@effect-agent/pr-review/ReviewCoverage",
|
|
28
59
|
)({
|
|
@@ -37,11 +68,62 @@ export class ReviewCoverage extends Schema.Class<ReviewCoverage>(
|
|
|
37
68
|
Schema.isMaxLength(300),
|
|
38
69
|
),
|
|
39
70
|
failedUnits: Schema.Array(FailedReviewUnit).check(Schema.isMaxLength(8)),
|
|
71
|
+
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1_000))).check(
|
|
72
|
+
Schema.isMaxLength(32),
|
|
73
|
+
),
|
|
74
|
+
}) {}
|
|
75
|
+
|
|
76
|
+
export class ReviewInputCoverage extends Schema.Class<ReviewInputCoverage>(
|
|
77
|
+
"@effect-agent/pr-review/ReviewInputCoverage",
|
|
78
|
+
)({
|
|
79
|
+
status: Schema.Literals(["complete", "incomplete"]),
|
|
80
|
+
requiredPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
|
|
81
|
+
Schema.isMaxLength(300),
|
|
82
|
+
),
|
|
83
|
+
assignedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
|
|
84
|
+
Schema.isMaxLength(300),
|
|
85
|
+
),
|
|
86
|
+
/** Assigned paths whose model-visible diff was truncated by the evidence bound. */
|
|
87
|
+
partialPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
|
|
88
|
+
Schema.isMaxLength(300),
|
|
89
|
+
),
|
|
90
|
+
unassignedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
|
|
91
|
+
Schema.isMaxLength(300),
|
|
92
|
+
),
|
|
40
93
|
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1_000))).check(
|
|
41
94
|
Schema.isMaxLength(20),
|
|
42
95
|
),
|
|
43
96
|
}) {}
|
|
44
97
|
|
|
98
|
+
export class FailedReviewPass extends Schema.Class<FailedReviewPass>(
|
|
99
|
+
"@effect-agent/pr-review/FailedReviewPass",
|
|
100
|
+
)({
|
|
101
|
+
workId: Schema.NonEmptyString.check(Schema.isMaxLength(96)),
|
|
102
|
+
stage: Schema.Literals(["discovery", "specialist", "verification"]),
|
|
103
|
+
errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
104
|
+
}) {}
|
|
105
|
+
|
|
106
|
+
export class ReviewAssurance extends Schema.Class<ReviewAssurance>(
|
|
107
|
+
"@effect-agent/pr-review/ReviewAssurance",
|
|
108
|
+
)({
|
|
109
|
+
status: Schema.Literals(["settled", "incomplete", "unverified"]),
|
|
110
|
+
requiredGeneralDiscoveryPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
111
|
+
completedGeneralDiscoveryPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
112
|
+
requiredSpecialistPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
113
|
+
completedSpecialistPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
114
|
+
requiredVerificationPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
115
|
+
completedVerificationPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
116
|
+
discoveredCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
117
|
+
confirmedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
118
|
+
rejectedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
119
|
+
unsettledCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
120
|
+
/** Every failure remains visible within the coordinator's 32-call hard bound. */
|
|
121
|
+
failedPasses: Schema.Array(FailedReviewPass).check(Schema.isMaxLength(64)),
|
|
122
|
+
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1_000))).check(
|
|
123
|
+
Schema.isMaxLength(32),
|
|
124
|
+
),
|
|
125
|
+
}) {}
|
|
126
|
+
|
|
45
127
|
interface ToolTrace {
|
|
46
128
|
readonly declared: Map<string, Extract<RunEvent, { readonly _tag: "ToolCallDeclared" }>>;
|
|
47
129
|
readonly succeeded: Map<string, Extract<RunEvent, { readonly _tag: "ToolCallSucceeded" }>>;
|
|
@@ -81,24 +163,30 @@ const boundedListReason = (label: string, values: Iterable<string>): string => {
|
|
|
81
163
|
return rendered;
|
|
82
164
|
};
|
|
83
165
|
|
|
84
|
-
const
|
|
166
|
+
const flatInputCoverage = (
|
|
85
167
|
files: ReadonlyArray<ChangedFile>,
|
|
86
168
|
totalFiles: number,
|
|
87
169
|
trace: ToolTrace,
|
|
88
|
-
):
|
|
170
|
+
): ReviewInputCoverage => {
|
|
89
171
|
const requiredPaths = sortedUnique(files.map((file) => file.path));
|
|
90
|
-
const
|
|
172
|
+
const assigned = new Set<string>();
|
|
173
|
+
const partial = new Set<string>();
|
|
91
174
|
const failedPaths = new Set<string>();
|
|
92
175
|
for (const [toolCallId, declaration] of trace.declared) {
|
|
93
176
|
if (declaration.toolName !== "read_file_diff") continue;
|
|
94
177
|
const query = Schema.decodeUnknownOption(FileDiffQuery)(declaration.parameters);
|
|
95
178
|
if (Option.isNone(query)) continue;
|
|
96
|
-
|
|
179
|
+
const success = trace.succeeded.get(toolCallId);
|
|
180
|
+
if (success !== undefined) {
|
|
181
|
+
assigned.add(query.value.path);
|
|
182
|
+
const view = Schema.decodeUnknownOption(FileDiffView)(success.result);
|
|
183
|
+
if (Option.isSome(view) && view.value.truncated) partial.add(query.value.path);
|
|
184
|
+
}
|
|
97
185
|
if (trace.failed.has(toolCallId)) failedPaths.add(query.value.path);
|
|
98
186
|
}
|
|
99
187
|
const undiffable = files.filter((file) => !isReviewableFile(file)).map((file) => file.path);
|
|
100
|
-
const
|
|
101
|
-
(path) => !
|
|
188
|
+
const unassigned = requiredPaths.filter(
|
|
189
|
+
(path) => !assigned.has(path) || undiffable.includes(path) || failedPaths.has(path),
|
|
102
190
|
);
|
|
103
191
|
const reasons: Array<string> = [];
|
|
104
192
|
if (files.length < totalFiles) {
|
|
@@ -109,92 +197,31 @@ const flatCoverage = (
|
|
|
109
197
|
boundedListReason("required paths have no reviewable diff or bounded text", undiffable),
|
|
110
198
|
);
|
|
111
199
|
}
|
|
112
|
-
if (failedPaths.size > 0)
|
|
113
|
-
|
|
200
|
+
if (failedPaths.size > 0) reasons.push(boundedListReason("diff reads failed", failedPaths));
|
|
201
|
+
if (partial.size > 0) {
|
|
202
|
+
reasons.push(boundedListReason("model-visible diff evidence was truncated", partial));
|
|
114
203
|
}
|
|
115
|
-
if (
|
|
116
|
-
reasons.push(boundedListReason("required paths
|
|
204
|
+
if (unassigned.length > 0) {
|
|
205
|
+
reasons.push(boundedListReason("required paths received no successful diff input", unassigned));
|
|
117
206
|
}
|
|
118
|
-
return
|
|
207
|
+
return ReviewInputCoverage.make({
|
|
119
208
|
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
120
209
|
requiredPaths,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
210
|
+
assignedPaths: sortedUnique(assigned),
|
|
211
|
+
partialPaths: sortedUnique(partial),
|
|
212
|
+
unassignedPaths: sortedUnique(unassigned),
|
|
124
213
|
reasons,
|
|
125
214
|
});
|
|
126
215
|
};
|
|
127
216
|
|
|
128
|
-
const
|
|
217
|
+
const fanOutInputCoverage = (
|
|
129
218
|
files: ReadonlyArray<ChangedFile>,
|
|
130
219
|
totalFiles: number,
|
|
131
|
-
|
|
132
|
-
): ReviewCoverage => {
|
|
220
|
+
): ReviewInputCoverage => {
|
|
133
221
|
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> = [];
|
|
222
|
+
const assignedPaths = sortedUnique(plan.units.flatMap((unit) => unit.paths));
|
|
223
|
+
const unassignedPaths = sortedUnique([...plan.undiffablePaths, ...plan.unassignedPaths]);
|
|
150
224
|
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
225
|
if (plan.truncated) {
|
|
199
226
|
reasons.push(`review range exposed ${files.length} of ${totalFiles} required files`);
|
|
200
227
|
}
|
|
@@ -206,78 +233,545 @@ const fanOutCoverage = (
|
|
|
206
233
|
),
|
|
207
234
|
);
|
|
208
235
|
}
|
|
209
|
-
if (plan.
|
|
210
|
-
reasons.push(
|
|
236
|
+
if (plan.partialEvidencePaths.length > 0) {
|
|
237
|
+
reasons.push(
|
|
238
|
+
boundedListReason(
|
|
239
|
+
"fan-out capacity left some deterministic evidence shards unassigned",
|
|
240
|
+
plan.partialEvidencePaths,
|
|
241
|
+
),
|
|
242
|
+
);
|
|
211
243
|
}
|
|
212
|
-
if (
|
|
244
|
+
if (plan.unassignedEvidenceShardCount > 0) {
|
|
245
|
+
reasons.push(
|
|
246
|
+
`${plan.unassignedEvidenceShardCount} deterministic evidence shard(s) exceeded fan-out capacity`,
|
|
247
|
+
);
|
|
213
248
|
reasons.push(
|
|
214
249
|
boundedListReason(
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
`unassigned evidence shard identifier sample (${plan.unassignedEvidenceShardIds.length} of ${plan.unassignedEvidenceShardCount})`,
|
|
251
|
+
plan.unassignedEvidenceShardIds,
|
|
217
252
|
),
|
|
218
253
|
);
|
|
219
254
|
}
|
|
220
|
-
|
|
255
|
+
if (plan.unassignedPaths.length > 0) {
|
|
256
|
+
reasons.push(boundedListReason("fan-out capacity left paths unassigned", plan.unassignedPaths));
|
|
257
|
+
}
|
|
258
|
+
return ReviewInputCoverage.make({
|
|
221
259
|
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
222
260
|
requiredPaths: sortedUnique(files.map((file) => file.path)),
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
261
|
+
assignedPaths,
|
|
262
|
+
partialPaths: plan.partialEvidencePaths,
|
|
263
|
+
unassignedPaths,
|
|
226
264
|
reasons,
|
|
227
265
|
});
|
|
228
266
|
};
|
|
229
267
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 [];
|
|
246
287
|
const request = Schema.decodeUnknownOption(FileReviewRequest)(declaration.parameters);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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)));
|
|
255
476
|
}
|
|
256
477
|
}
|
|
257
|
-
|
|
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,
|
|
656
|
+
reasons,
|
|
657
|
+
}),
|
|
658
|
+
confirmedFindings: confirmedCandidates.flatMap(({ assessment, candidate }) =>
|
|
659
|
+
candidate._tag === "FindingCandidate"
|
|
660
|
+
? [confirmedFindingForPublication(assessment, candidate)]
|
|
661
|
+
: [],
|
|
662
|
+
),
|
|
663
|
+
confirmedConcerns: confirmedCandidates.flatMap(({ candidate }) =>
|
|
664
|
+
candidate._tag === "ConcernCandidate" ? [candidate.concern] : [],
|
|
665
|
+
),
|
|
666
|
+
walkthrough,
|
|
667
|
+
};
|
|
258
668
|
};
|
|
259
669
|
|
|
260
|
-
|
|
261
|
-
|
|
670
|
+
const compatibilityCoverage = (
|
|
671
|
+
inputCoverage: ReviewInputCoverage,
|
|
672
|
+
assurance: ReviewAssurance,
|
|
673
|
+
): ReviewCoverage => {
|
|
674
|
+
const assuranceIncomplete = assurance.status === "incomplete";
|
|
675
|
+
const failedUnits = new Map<string, FailedReviewUnit>();
|
|
676
|
+
for (const pass of assurance.failedPasses) {
|
|
677
|
+
const unitId = pass.workId.slice(0, "unit-000".length);
|
|
678
|
+
if (!failedUnits.has(unitId)) {
|
|
679
|
+
failedUnits.set(
|
|
680
|
+
unitId,
|
|
681
|
+
FailedReviewUnit.make({ unitId, errorTag: `${pass.stage}:${pass.errorTag}` }),
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return ReviewCoverage.make({
|
|
686
|
+
status: inputCoverage.status === "complete" && !assuranceIncomplete ? "complete" : "incomplete",
|
|
687
|
+
requiredPaths: inputCoverage.requiredPaths,
|
|
688
|
+
reviewedPaths: inputCoverage.assignedPaths,
|
|
689
|
+
unreviewedPaths: sortedUnique([
|
|
690
|
+
...inputCoverage.partialPaths,
|
|
691
|
+
...inputCoverage.unassignedPaths,
|
|
692
|
+
]),
|
|
693
|
+
failedUnits: [...failedUnits.values()].slice(0, 8),
|
|
694
|
+
reasons: [...inputCoverage.reasons, ...(assuranceIncomplete ? assurance.reasons : [])],
|
|
695
|
+
});
|
|
696
|
+
};
|
|
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: {
|
|
262
710
|
readonly shape: ReviewShape;
|
|
263
711
|
readonly files: ReadonlyArray<ChangedFile>;
|
|
264
712
|
readonly totalFiles: number;
|
|
265
713
|
readonly anchorFiles: ReadonlyArray<ChangedFile>;
|
|
266
714
|
readonly totalAnchorFiles: number;
|
|
267
715
|
readonly events: ReadonlyArray<RunEvent>;
|
|
268
|
-
}):
|
|
716
|
+
}): ReviewPipelineAssessment => {
|
|
269
717
|
const trace = toolTrace(input.events);
|
|
270
|
-
|
|
718
|
+
let inputCoverage =
|
|
271
719
|
input.shape === "fan-out"
|
|
272
|
-
?
|
|
273
|
-
:
|
|
274
|
-
if (input.anchorFiles.length
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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));
|
|
282
776
|
});
|
|
283
777
|
};
|