@effect-agent/pr-review 0.1.0-beta.22 → 0.1.0-beta.23

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.
@@ -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
- 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";
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 configured discovery/specialist pass and every
37
- // candidate-verification batch settled exactly.
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
- /** Every failure remains visible within the coordinator's 32-call hard bound. */
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
- const boundedListReason = (label: string, values: Iterable<string>): string => {
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 flatInputCoverage = (
167
- files: ReadonlyArray<ChangedFile>,
168
- totalFiles: number,
169
- trace: ToolTrace,
170
- ): ReviewInputCoverage => {
171
- const requiredPaths = sortedUnique(files.map((file) => file.path));
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 = files.filter((file) => !isReviewableFile(file)).map((file) => file.path);
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) => !assigned.has(path) || undiffable.includes(path) || failedPaths.has(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(`review range exposed ${files.length} of ${totalFiles} required files`);
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.length > 0) {
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
- return ReviewInputCoverage.make({
208
- status: reasons.length === 0 ? "complete" : "incomplete",
209
- requiredPaths,
210
- assignedPaths: sortedUnique(assigned),
211
- partialPaths: sortedUnique(partial),
212
- unassignedPaths: sortedUnique(unassigned),
213
- reasons,
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
- const fanOutInputCoverage = (
218
- files: ReadonlyArray<ChangedFile>,
219
- totalFiles: number,
220
- ): ReviewInputCoverage => {
221
- const plan = planReviewUnits(files, { totalChangedFiles: totalFiles });
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([...plan.undiffablePaths, ...plan.unassignedPaths]);
297
+ const unassignedPaths = sortedUnique(plan.unassignedPaths);
224
298
  const reasons: Array<string> = [];
225
299
  if (plan.truncated) {
226
- reasons.push(`review range exposed ${files.length} of ${totalFiles} required files`);
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 ReviewInputCoverage.make({
259
- status: reasons.length === 0 ? "complete" : "incomplete",
260
- requiredPaths: sortedUnique(files.map((file) => file.path)),
261
- assignedPaths,
262
- partialPaths: plan.partialEvidencePaths,
263
- unassignedPaths,
264
- reasons,
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
- 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
- };
344
+ input.anchorFiles,
345
+ input.totalAnchorFiles,
346
+ );
668
347
  };
669
348
 
670
- const compatibilityCoverage = (
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
- };