@effect-agent/pr-review 0.1.0-beta.21 → 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.
- package/README.md +63 -46
- package/dist/action.d.mts +13 -13
- package/dist/action.mjs +33 -25
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-D5xrmadQ.d.mts → fan-out-BJBTAYuh.d.mts} +278 -303
- package/dist/{github-DSqZp3Ce.mjs → github-BbwYzNrC.mjs} +588 -243
- package/dist/github-BbwYzNrC.mjs.map +1 -0
- package/dist/index.d.mts +72 -116
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-CblG1G9b.mjs → providers-NyP-4rS6.mjs} +162 -578
- package/dist/providers-NyP-4rS6.mjs.map +1 -0
- package/dist/testing.d.mts +4 -15
- package/dist/testing.mjs +12 -41
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -34
- package/src/internal/action-entry.ts +0 -1
- package/src/internal/coverage.ts +147 -532
- package/src/internal/factory.ts +29 -50
- package/src/internal/fan-out-scripted.ts +26 -80
- package/src/internal/fan-out.ts +577 -426
- package/src/internal/profiles.ts +8 -8
- package/src/internal/render.ts +34 -45
- package/src/internal/retirement.ts +3 -1
- package/src/internal/review-agent.ts +6 -1
- package/src/internal/review-state.ts +43 -19
- package/src/internal/review-units.ts +25 -0
- package/src/internal/run.ts +211 -174
- package/src/internal/source.ts +1 -1
- package/dist/github-DSqZp3Ce.mjs.map +0 -1
- package/dist/providers-CblG1G9b.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context, DateTime, Effect, Encoding, Layer, Option, Redacted, Result, Schema } from "effect";
|
|
2
|
-
import { Agent, AgentPolicy,
|
|
2
|
+
import { Agent, AgentPolicy, AgentRuntime, ToolExecutionClass, ToolResultBounds } from "effect-agent";
|
|
3
3
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
4
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
5
5
|
//#region src/internal/diff.ts
|
|
@@ -483,7 +483,7 @@ var ReviewFinding = class extends Schema.Class("@effect-agent/pr-review/ReviewFi
|
|
|
483
483
|
title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
|
|
484
484
|
body: Schema.NonEmptyString.check(Schema.isMaxLength(2e3)),
|
|
485
485
|
/** Replacement for exactly lines startLine..endLine; omit when unsure. */
|
|
486
|
-
suggestion: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(2e3)))
|
|
486
|
+
suggestion: Schema.optionalKey(Schema.String.annotate({ description: "Committable replacement source code for exactly lines startLine..endLine: the full replacement for every line in the range and nothing else — never prose describing the change, which belongs in body." }).check(Schema.isMaxLength(2e3)))
|
|
487
487
|
}) {};
|
|
488
488
|
const ReviewVerdict = Schema.Literals([
|
|
489
489
|
"approve",
|
|
@@ -579,6 +579,228 @@ const PullRequestReviewer = Agent.define("pr-reviewer", {
|
|
|
579
579
|
}
|
|
580
580
|
});
|
|
581
581
|
//#endregion
|
|
582
|
+
//#region src/internal/coverage.ts
|
|
583
|
+
var FailedReviewUnit = class extends Schema.Class("@effect-agent/pr-review/FailedReviewUnit")({
|
|
584
|
+
unitId: Schema.NonEmptyString.check(Schema.isMaxLength(32)),
|
|
585
|
+
errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256))
|
|
586
|
+
}) {};
|
|
587
|
+
/**
|
|
588
|
+
* Compatibility diagnostic retained for callers that consumed the original
|
|
589
|
+
* `coverage` field. New UI and state decisions use ReviewInputCoverage and
|
|
590
|
+
* ReviewAssurance directly.
|
|
591
|
+
*/
|
|
592
|
+
var ReviewCoverage = class extends Schema.Class("@effect-agent/pr-review/ReviewCoverage")({
|
|
593
|
+
status: Schema.Literals(["complete", "incomplete"]),
|
|
594
|
+
requiredPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
595
|
+
reviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
596
|
+
unreviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
597
|
+
failedUnits: Schema.Array(FailedReviewUnit).check(Schema.isMaxLength(8)),
|
|
598
|
+
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1e3))).check(Schema.isMaxLength(32))
|
|
599
|
+
}) {};
|
|
600
|
+
var ReviewInputCoverage = class extends Schema.Class("@effect-agent/pr-review/ReviewInputCoverage")({
|
|
601
|
+
status: Schema.Literals(["complete", "incomplete"]),
|
|
602
|
+
requiredPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
603
|
+
assignedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
604
|
+
/** Assigned paths whose model-visible diff was truncated by the evidence bound. */
|
|
605
|
+
partialPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
606
|
+
unassignedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
607
|
+
/**
|
|
608
|
+
* Paths with neither a textual diff nor bounded base/head text (binaries,
|
|
609
|
+
* oversized files). Fail-closed: they keep the status incomplete for as
|
|
610
|
+
* long as they are part of the pull request — an unreviewable change must
|
|
611
|
+
* never authorize a green check. Exclude them deliberately with ignore
|
|
612
|
+
* globs when that is intended.
|
|
613
|
+
*/
|
|
614
|
+
undiffablePaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
615
|
+
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1e3))).check(Schema.isMaxLength(20))
|
|
616
|
+
}) {};
|
|
617
|
+
var FailedReviewPass = class extends Schema.Class("@effect-agent/pr-review/FailedReviewPass")({
|
|
618
|
+
workId: Schema.NonEmptyString.check(Schema.isMaxLength(96)),
|
|
619
|
+
stage: Schema.Literals([
|
|
620
|
+
"discovery",
|
|
621
|
+
"specialist",
|
|
622
|
+
"verification"
|
|
623
|
+
]),
|
|
624
|
+
errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256))
|
|
625
|
+
}) {};
|
|
626
|
+
/**
|
|
627
|
+
* Settlement of scheduled review work. `incomplete` means reviewer-side work
|
|
628
|
+
* failed after its bounded retry — a machinery gap that is carried forward and
|
|
629
|
+
* retried on the next run, never a statement about the code under review.
|
|
630
|
+
* `unverified` is the flat reviewer's honest constant: one pass with no
|
|
631
|
+
* independent verifier is neither settled assurance nor a failure.
|
|
632
|
+
*/
|
|
633
|
+
var ReviewAssurance = class extends Schema.Class("@effect-agent/pr-review/ReviewAssurance")({
|
|
634
|
+
status: Schema.Literals([
|
|
635
|
+
"settled",
|
|
636
|
+
"incomplete",
|
|
637
|
+
"unverified"
|
|
638
|
+
]),
|
|
639
|
+
requiredGeneralDiscoveryPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
640
|
+
completedGeneralDiscoveryPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
641
|
+
requiredSpecialistPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
642
|
+
completedSpecialistPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
643
|
+
requiredVerificationPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
644
|
+
completedVerificationPasses: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
645
|
+
discoveredCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
646
|
+
confirmedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
647
|
+
rejectedCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
648
|
+
unsettledCandidates: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
649
|
+
/** Discovery claims discarded for anchors/paths outside their assigned evidence. */
|
|
650
|
+
discardedInvalidFindings: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
651
|
+
failedPasses: Schema.Array(FailedReviewPass).check(Schema.isMaxLength(64)),
|
|
652
|
+
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1e3))).check(Schema.isMaxLength(32))
|
|
653
|
+
}) {};
|
|
654
|
+
const toolTrace = (events) => {
|
|
655
|
+
const declared = /* @__PURE__ */ new Map();
|
|
656
|
+
const succeeded = /* @__PURE__ */ new Map();
|
|
657
|
+
const failed = /* @__PURE__ */ new Map();
|
|
658
|
+
for (const event of events) {
|
|
659
|
+
if (event._tag === "ToolCallDeclared") declared.set(event.toolCallId, event);
|
|
660
|
+
if (event._tag === "ToolCallSucceeded") succeeded.set(event.toolCallId, event);
|
|
661
|
+
if (event._tag === "ToolCallFailed") failed.set(event.toolCallId, event);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
declared,
|
|
665
|
+
succeeded,
|
|
666
|
+
failed
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
const sortedUnique = (values) => [...new Set(values)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
670
|
+
/** Render a bounded, deterministic "label (n): a, b, … (+k more)" reason line. */
|
|
671
|
+
const boundedListReason = (label, values) => {
|
|
672
|
+
const items = sortedUnique(values);
|
|
673
|
+
let rendered = `${label} (${items.length}): `;
|
|
674
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
675
|
+
const item = items[index] ?? "";
|
|
676
|
+
const separator = index === 0 ? "" : ", ";
|
|
677
|
+
const omitted = items.length - index - 1;
|
|
678
|
+
const suffix = omitted === 0 ? "" : ` … (+${omitted} more)`;
|
|
679
|
+
if (`${rendered}${separator}${item}${suffix}`.length > 1e3) {
|
|
680
|
+
const omission = `… (+${items.length - index} more)`;
|
|
681
|
+
return `${rendered.slice(0, 1e3 - omission.length)}${omission}`;
|
|
682
|
+
}
|
|
683
|
+
rendered = `${rendered}${separator}${item}`;
|
|
684
|
+
}
|
|
685
|
+
return rendered;
|
|
686
|
+
};
|
|
687
|
+
const anchorSurfaceAdjusted = (inputCoverage, anchorFiles, totalAnchorFiles) => anchorFiles.length >= totalAnchorFiles ? inputCoverage : ReviewInputCoverage.make({
|
|
688
|
+
...inputCoverage,
|
|
689
|
+
status: "incomplete",
|
|
690
|
+
reasons: [...inputCoverage.reasons, `full pull-request anchor surface exposed ${anchorFiles.length} of ${totalAnchorFiles} required files`]
|
|
691
|
+
});
|
|
692
|
+
/** The flat reviewer's honest constant assurance: one pass, no verifier. */
|
|
693
|
+
const flatAssurance = () => ReviewAssurance.make({
|
|
694
|
+
status: "unverified",
|
|
695
|
+
requiredGeneralDiscoveryPasses: 1,
|
|
696
|
+
completedGeneralDiscoveryPasses: 1,
|
|
697
|
+
requiredSpecialistPasses: 0,
|
|
698
|
+
completedSpecialistPasses: 0,
|
|
699
|
+
requiredVerificationPasses: 0,
|
|
700
|
+
completedVerificationPasses: 0,
|
|
701
|
+
discoveredCandidates: 0,
|
|
702
|
+
confirmedCandidates: 0,
|
|
703
|
+
rejectedCandidates: 0,
|
|
704
|
+
unsettledCandidates: 0,
|
|
705
|
+
discardedInvalidFindings: 0,
|
|
706
|
+
failedPasses: [],
|
|
707
|
+
reasons: ["flat review has no independent candidate-verification pass; use the fan-out pipeline for a settled assurance result"]
|
|
708
|
+
});
|
|
709
|
+
/**
|
|
710
|
+
* Assess one settled flat run from its Run event trace: which required paths
|
|
711
|
+
* received successful bounded diff evidence. This observes tool INPUT
|
|
712
|
+
* assignment only — the host cannot know which evidence the model weighed.
|
|
713
|
+
*/
|
|
714
|
+
const assessFlatReview = (input) => {
|
|
715
|
+
const trace = toolTrace(input.events);
|
|
716
|
+
const requiredPaths = sortedUnique(input.files.map((file) => file.path));
|
|
717
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
718
|
+
const partial = /* @__PURE__ */ new Set();
|
|
719
|
+
const failedPaths = /* @__PURE__ */ new Set();
|
|
720
|
+
for (const [toolCallId, declaration] of trace.declared) {
|
|
721
|
+
if (declaration.toolName !== "read_file_diff") continue;
|
|
722
|
+
const query = Schema.decodeUnknownOption(FileDiffQuery)(declaration.parameters);
|
|
723
|
+
if (Option.isNone(query)) continue;
|
|
724
|
+
const success = trace.succeeded.get(toolCallId);
|
|
725
|
+
if (success !== void 0) {
|
|
726
|
+
assigned.add(query.value.path);
|
|
727
|
+
const view = Schema.decodeUnknownOption(FileDiffView)(success.result);
|
|
728
|
+
if (Option.isSome(view) && view.value.truncated) partial.add(query.value.path);
|
|
729
|
+
}
|
|
730
|
+
if (trace.failed.has(toolCallId)) failedPaths.add(query.value.path);
|
|
731
|
+
}
|
|
732
|
+
const undiffable = new Set(input.files.filter((file) => !isReviewableFile(file)).map((file) => file.path));
|
|
733
|
+
const unassigned = requiredPaths.filter((path) => !undiffable.has(path) && (!assigned.has(path) || failedPaths.has(path)));
|
|
734
|
+
const reasons = [];
|
|
735
|
+
if (input.files.length < input.totalFiles) reasons.push(`review range exposed ${input.files.length} of ${input.totalFiles} required files`);
|
|
736
|
+
if (undiffable.size > 0) reasons.push(boundedListReason("required paths have no reviewable diff or bounded text", undiffable));
|
|
737
|
+
if (failedPaths.size > 0) reasons.push(boundedListReason("diff reads failed", failedPaths));
|
|
738
|
+
if (partial.size > 0) reasons.push(boundedListReason("model-visible diff evidence was truncated", partial));
|
|
739
|
+
if (unassigned.length > 0) reasons.push(boundedListReason("required paths received no successful diff input", unassigned));
|
|
740
|
+
return {
|
|
741
|
+
inputCoverage: anchorSurfaceAdjusted(ReviewInputCoverage.make({
|
|
742
|
+
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
743
|
+
requiredPaths,
|
|
744
|
+
assignedPaths: sortedUnique(assigned),
|
|
745
|
+
partialPaths: sortedUnique(partial),
|
|
746
|
+
unassignedPaths: sortedUnique(unassigned),
|
|
747
|
+
undiffablePaths: sortedUnique(undiffable),
|
|
748
|
+
reasons
|
|
749
|
+
}), input.anchorFiles, input.totalAnchorFiles),
|
|
750
|
+
assurance: flatAssurance(),
|
|
751
|
+
unreviewedPaths: sortedUnique([...unassigned, ...undiffable])
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
/**
|
|
755
|
+
* Input coverage of one host-scheduled fan-out plan: which required paths the
|
|
756
|
+
* bounded plan actually assigned complete evidence for. Capacity overflow and
|
|
757
|
+
* undiffable paths are both real gaps; the pipeline carries them so the check
|
|
758
|
+
* stays fail-closed until they are reviewed, removed, or explicitly ignored.
|
|
759
|
+
*/
|
|
760
|
+
const fanOutInputCoverage = (input) => {
|
|
761
|
+
const plan = input.plan;
|
|
762
|
+
const assignedPaths = sortedUnique(plan.units.flatMap((unit) => unit.paths));
|
|
763
|
+
const unassignedPaths = sortedUnique(plan.unassignedPaths);
|
|
764
|
+
const reasons = [];
|
|
765
|
+
if (plan.truncated) reasons.push(`review range exposed ${input.files.length} of ${input.totalFiles} required files`);
|
|
766
|
+
if (plan.undiffablePaths.length > 0) reasons.push(boundedListReason("required paths have no reviewable diff or bounded text", plan.undiffablePaths));
|
|
767
|
+
if (plan.partialEvidencePaths.length > 0) reasons.push(boundedListReason("fan-out capacity left some deterministic evidence shards unassigned", plan.partialEvidencePaths));
|
|
768
|
+
if (plan.unassignedEvidenceShardCount > 0) {
|
|
769
|
+
reasons.push(`${plan.unassignedEvidenceShardCount} deterministic evidence shard(s) exceeded fan-out capacity`);
|
|
770
|
+
reasons.push(boundedListReason(`unassigned evidence shard identifier sample (${plan.unassignedEvidenceShardIds.length} of ${plan.unassignedEvidenceShardCount})`, plan.unassignedEvidenceShardIds));
|
|
771
|
+
}
|
|
772
|
+
if (plan.unassignedPaths.length > 0) reasons.push(boundedListReason("fan-out capacity left paths unassigned", plan.unassignedPaths));
|
|
773
|
+
return anchorSurfaceAdjusted(ReviewInputCoverage.make({
|
|
774
|
+
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
775
|
+
requiredPaths: sortedUnique(input.files.map((file) => file.path)),
|
|
776
|
+
assignedPaths,
|
|
777
|
+
partialPaths: plan.partialEvidencePaths,
|
|
778
|
+
unassignedPaths,
|
|
779
|
+
undiffablePaths: sortedUnique(plan.undiffablePaths),
|
|
780
|
+
reasons
|
|
781
|
+
}), input.anchorFiles, input.totalAnchorFiles);
|
|
782
|
+
};
|
|
783
|
+
/** Compatibility aggregate over the two precise claims. */
|
|
784
|
+
const compatibilityCoverage = (inputCoverage, assurance) => {
|
|
785
|
+
const assuranceIncomplete = assurance.status === "incomplete";
|
|
786
|
+
const failedUnits = /* @__PURE__ */ new Map();
|
|
787
|
+
for (const pass of assurance.failedPasses) {
|
|
788
|
+
const unitId = pass.workId.slice(0, 8);
|
|
789
|
+
if (!failedUnits.has(unitId)) failedUnits.set(unitId, FailedReviewUnit.make({
|
|
790
|
+
unitId,
|
|
791
|
+
errorTag: `${pass.stage}:${pass.errorTag}`
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
return ReviewCoverage.make({
|
|
795
|
+
status: inputCoverage.status === "complete" && !assuranceIncomplete ? "complete" : "incomplete",
|
|
796
|
+
requiredPaths: inputCoverage.requiredPaths,
|
|
797
|
+
reviewedPaths: inputCoverage.assignedPaths,
|
|
798
|
+
unreviewedPaths: sortedUnique([...inputCoverage.partialPaths, ...inputCoverage.unassignedPaths]),
|
|
799
|
+
failedUnits: [...failedUnits.values()].slice(0, 8),
|
|
800
|
+
reasons: [...inputCoverage.reasons, ...assuranceIncomplete ? assurance.reasons : []]
|
|
801
|
+
});
|
|
802
|
+
};
|
|
803
|
+
//#endregion
|
|
582
804
|
//#region src/internal/review-units.ts
|
|
583
805
|
/** The delegation fan-out bound: one parent Run spawns at most this many children. */
|
|
584
806
|
const MAX_REVIEW_UNITS = 8;
|
|
@@ -928,6 +1150,20 @@ const rankAndDedupeFindings = (findings) => {
|
|
|
928
1150
|
return left.startLine - right.startLine;
|
|
929
1151
|
}).slice(0, 20);
|
|
930
1152
|
};
|
|
1153
|
+
/**
|
|
1154
|
+
* The concern analogue of `rankAndDedupeFindings`: dedupe by exact content
|
|
1155
|
+
* keeping the most severe duplicate, rank by severity, and cap at the
|
|
1156
|
+
* `CodeReview` concerns bound.
|
|
1157
|
+
*/
|
|
1158
|
+
const rankAndDedupeConcerns = (concerns) => {
|
|
1159
|
+
const byContent = /* @__PURE__ */ new Map();
|
|
1160
|
+
for (const concern of concerns) {
|
|
1161
|
+
const key = `${concern.title}\u0000${concern.body}`;
|
|
1162
|
+
const previous = byContent.get(key);
|
|
1163
|
+
if (previous === void 0 || severityRank[concern.severity] < severityRank[previous.severity]) byContent.set(key, concern);
|
|
1164
|
+
}
|
|
1165
|
+
return [...byContent.values()].sort((left, right) => severityRank[left.severity] - severityRank[right.severity]).slice(0, 10);
|
|
1166
|
+
};
|
|
931
1167
|
//#endregion
|
|
932
1168
|
//#region src/internal/fan-out.ts
|
|
933
1169
|
/** One discovery pass returns at most this many anchored candidates. */
|
|
@@ -936,8 +1172,14 @@ const MAX_CHILD_FINDINGS = 6;
|
|
|
936
1172
|
const MAX_CHILD_CONCERNS = 3;
|
|
937
1173
|
/** Every unit receives independent general and specialist discovery passes. */
|
|
938
1174
|
const MAX_UNIT_CANDIDATES = 18;
|
|
939
|
-
/**
|
|
1175
|
+
/**
|
|
1176
|
+
* General + specialist discovery for every unit, then one verifier per unit.
|
|
1177
|
+
* The one-retry budget doubles the worst-case child Run count, but the
|
|
1178
|
+
* schedule itself never exceeds this bound.
|
|
1179
|
+
*/
|
|
940
1180
|
const MAX_REVIEW_CHILDREN = 24;
|
|
1181
|
+
/** Bounded structured concurrency across units; passes inside a unit are sequential. */
|
|
1182
|
+
const REVIEW_UNIT_CONCURRENCY = 4;
|
|
941
1183
|
/** Structural minimum for a child that exposes no tools. */
|
|
942
1184
|
const MAX_FILE_REVIEW_TOOL_CALLS = 1;
|
|
943
1185
|
const ReviewWorkPhase = Schema.Literals(["discovery", "verification"]);
|
|
@@ -967,9 +1209,33 @@ const reviewCandidateSubjectKey = (candidate) => candidate._tag === "FindingCand
|
|
|
967
1209
|
var CandidateAssessment = class extends Schema.Class("@effect-agent/pr-review/CandidateAssessment")({
|
|
968
1210
|
candidateId: ReviewCandidateId,
|
|
969
1211
|
disposition: Schema.Literals(["confirmed", "rejected"]),
|
|
1212
|
+
/**
|
|
1213
|
+
* Exact suggestion settlement: required when the candidate finding carries
|
|
1214
|
+
* a suggestion, forbidden otherwise. Untrusted child output cannot publish
|
|
1215
|
+
* a GitHub replacement block by prompt compliance alone — the host keeps a
|
|
1216
|
+
* confirmed finding's suggestion only on an exact "committable" settlement.
|
|
1217
|
+
*/
|
|
1218
|
+
suggestion: Schema.optionalKey(Schema.Literals(["committable", "not-committable"]).annotate({ description: "Required exactly when the candidate finding carries a suggestion: \"committable\" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else. Forbidden for candidates without a suggestion." })),
|
|
970
1219
|
rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600))
|
|
971
1220
|
}) {};
|
|
972
1221
|
/**
|
|
1222
|
+
* Exact suggestion settlement shape: a carried suggestion must be settled and
|
|
1223
|
+
* nothing else may be. A verification report that violates it is treated as a
|
|
1224
|
+
* misbehaving pass and retried within the pass budget.
|
|
1225
|
+
*/
|
|
1226
|
+
const assessmentSettlesSuggestionExactly = (assessment, candidate) => candidate._tag === "FindingCandidate" && candidate.finding.suggestion !== void 0 ? assessment.suggestion !== void 0 : assessment.suggestion === void 0;
|
|
1227
|
+
/**
|
|
1228
|
+
* Fail-closed publication of a confirmed finding: only an exact "committable"
|
|
1229
|
+
* settlement keeps the suggestion; anything else publishes the finding with
|
|
1230
|
+
* the suggestion stripped so unverified text can never become a one-click
|
|
1231
|
+
* GitHub replacement block.
|
|
1232
|
+
*/
|
|
1233
|
+
const confirmedFindingForPublication = (assessment, candidate) => {
|
|
1234
|
+
if (candidate.finding.suggestion === void 0 || assessment.suggestion === "committable") return candidate.finding;
|
|
1235
|
+
const { suggestion: _stripped, ...finding } = candidate.finding;
|
|
1236
|
+
return ReviewFinding.make(finding);
|
|
1237
|
+
};
|
|
1238
|
+
/**
|
|
973
1239
|
* Concern candidates need explicit paths internally to bind the claim to
|
|
974
1240
|
* scheduled evidence. The verifier receives the complete bounded unit so it
|
|
975
1241
|
* can use neighboring evidence to falsify the claim. The public ReviewConcern
|
|
@@ -983,18 +1249,6 @@ const UnitPaths = Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(S
|
|
|
983
1249
|
const RiskCategories = Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6));
|
|
984
1250
|
const Candidates = Schema.Array(ReviewCandidate).check(Schema.isMaxLength(18));
|
|
985
1251
|
const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
|
|
986
|
-
/** Strict-object coordinator request for either discovery or verification. */
|
|
987
|
-
var FileReviewRequest = class extends Schema.Class("@effect-agent/pr-review/FileReviewRequest")({
|
|
988
|
-
phase: ReviewWorkPhase,
|
|
989
|
-
workId: ReviewPassId,
|
|
990
|
-
unitId: ReviewUnitId,
|
|
991
|
-
paths: UnitPaths,
|
|
992
|
-
evidenceShardIds: EvidenceShardIds,
|
|
993
|
-
perspective: ReviewWorkPerspective,
|
|
994
|
-
riskCategories: RiskCategories,
|
|
995
|
-
/** Empty for discovery; the exact discovered set for unit verification. */
|
|
996
|
-
candidates: Candidates
|
|
997
|
-
}) {};
|
|
998
1252
|
/** One complete host-selected evidence shard supplied to a review child. */
|
|
999
1253
|
var FileReviewEvidence = class extends Schema.Class("@effect-agent/pr-review/FileReviewEvidence")({
|
|
1000
1254
|
shardId: ReviewEvidenceShardId,
|
|
@@ -1018,6 +1272,7 @@ var FileReviewBrief = class extends Schema.Class("@effect-agent/pr-review/FileRe
|
|
|
1018
1272
|
evidenceShardIds: EvidenceShardIds,
|
|
1019
1273
|
perspective: ReviewWorkPerspective,
|
|
1020
1274
|
riskCategories: RiskCategories,
|
|
1275
|
+
/** Empty for discovery; the exact discovered set for unit verification. */
|
|
1021
1276
|
candidates: Candidates,
|
|
1022
1277
|
evidence: Schema.Array(FileReviewEvidence).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12))
|
|
1023
1278
|
}) {};
|
|
@@ -1031,24 +1286,16 @@ var FileReviewReport = class extends Schema.Class("@effect-agent/pr-review/FileR
|
|
|
1031
1286
|
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)),
|
|
1032
1287
|
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(18))
|
|
1033
1288
|
}) {};
|
|
1034
|
-
/**
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(18))
|
|
1042
|
-
}) {};
|
|
1043
|
-
var FileReviewUnitFailed = class extends Schema.TaggedError()("FileReviewUnitFailed", {
|
|
1044
|
-
childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
1045
|
-
message: Schema.String.check(Schema.isMaxLength(400))
|
|
1046
|
-
}) {};
|
|
1047
|
-
var FileReviewWorkRejected = class extends Schema.TaggedError()("FileReviewWorkRejected", {
|
|
1289
|
+
/**
|
|
1290
|
+
* A structurally valid child report that does not answer the scheduled pass:
|
|
1291
|
+
* wrong identity, phase-inapplicable fields, or an inexact assessment set.
|
|
1292
|
+
* Retried once like any other pass fault, because it is model misbehavior,
|
|
1293
|
+
* not evidence about the code under review.
|
|
1294
|
+
*/
|
|
1295
|
+
var ReviewPassMisbehaved = class extends Schema.TaggedError()("ReviewPassMisbehaved", {
|
|
1048
1296
|
workId: ReviewPassId,
|
|
1049
1297
|
reason: Schema.NonEmptyString.check(Schema.isMaxLength(600))
|
|
1050
1298
|
}) {};
|
|
1051
|
-
const FileReviewFailure = Schema.Union([FileReviewUnitFailed, FileReviewWorkRejected]);
|
|
1052
1299
|
const staticGuidanceLines = (guidance) => {
|
|
1053
1300
|
if (guidance === void 0) return [];
|
|
1054
1301
|
return (typeof guidance === "string" ? [guidance] : guidance).filter((line) => line.length > 0);
|
|
@@ -1070,7 +1317,8 @@ const makeFileReviewerInstructions = (options = {}) => (brief) => {
|
|
|
1070
1317
|
"Independently verify every candidate in the input. You did not receive another reviewer's transcript or reasoning; use only the candidate claim and bounded evidence.",
|
|
1071
1318
|
"The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
|
|
1072
1319
|
"For each candidate, try to falsify it first. Confirm only when the cited behavior is supported and actionable. Reject unsupported, speculative, duplicate, or non-actionable candidates.",
|
|
1073
|
-
"Return ONLY JSON with phase \"verification\", the exact workId/unitId, empty findings/concerns/fileSummaries arrays, and exactly one assessment per candidateId. Each assessment is {\"candidateId\": <exact id>, \"disposition\": <\"confirmed\" | \"rejected\">, \"rationale\": <bounded evidence-based reason>}. Never add or omit an id."
|
|
1320
|
+
"Return ONLY JSON with phase \"verification\", the exact workId/unitId, empty findings/concerns/fileSummaries arrays, and exactly one assessment per candidateId. Each assessment is {\"candidateId\": <exact id>, \"disposition\": <\"confirmed\" | \"rejected\">, \"suggestion\": <\"committable\" | \"not-committable\", present exactly when the candidate finding carries a suggestion>, \"rationale\": <bounded evidence-based reason>}. Never add or omit an id.",
|
|
1321
|
+
"Settle every carried suggestion independently of the claim: answer \"committable\" only when its text is the full replacement source for exactly lines startLine..endLine and nothing else — it compiles in context and preserves the finding's intent, never prose describing a change. Otherwise answer \"not-committable\"; the host then publishes the confirmed finding without its suggestion. Omit the assessment \"suggestion\" field for candidates without one."
|
|
1074
1322
|
].join("\n");
|
|
1075
1323
|
const focus = brief.perspective === "risk-specialist" ? brief.riskCategories.length > 0 ? `This is a fresh specialist discovery pass. Concentrate on these host-classified risks without relying on another pass: ${brief.riskCategories.join(", ")}.` : "This is a fresh specialist discovery pass. The host found no keyword-classified category, so independently scrutinize authentication/authorization, security boundaries, durability, concurrency, credentials, and external side effects rather than treating classification silence as low risk." : "This is the general discovery pass. Review broadly for correctness, security, concurrency, resource, API, and error-handling defects.";
|
|
1076
1324
|
return [
|
|
@@ -1078,13 +1326,13 @@ const makeFileReviewerInstructions = (options = {}) => (brief) => {
|
|
|
1078
1326
|
focus,
|
|
1079
1327
|
"The discovery evidence array contains every complete shard in the unit. Review every entry and every shard of a multi-shard path. A later independent verifier, not you, decides which candidates publish.",
|
|
1080
1328
|
"When a non-anchored concern depends on one or more unit files, list 1-3 exact evidencePaths to bind the claim to scheduled evidence.",
|
|
1081
|
-
`Return ONLY JSON with phase "discovery", the exact workId/unitId, up to 6 findings, up to 3 concerns shaped as {concern, evidencePaths}, one factual file summary per path (<= 240 chars), and an empty assessments array. Empty candidate arrays are valid; do not invent defects
|
|
1329
|
+
`Return ONLY JSON with phase "discovery", the exact workId/unitId, up to 6 findings, up to 3 concerns shaped as {concern, evidencePaths}, one factual file summary per path (<= 240 chars), and an empty assessments array. Empty candidate arrays are valid; do not invent defects.`,
|
|
1330
|
+
"Each finding is {\"path\": <a unit file path>, \"startLine\": <integer, an R-marked new-file line>, \"endLine\": <integer, >= startLine, same file, R-marked>, \"severity\": <\"blocking\" | \"important\" | \"nit\">, \"category\": <OPTIONAL problem-kind label>, \"title\": <string, <= 120 chars>, \"body\": <string, why it matters and what to do>, \"suggestion\": <string, OPTIONAL: replacement source code for exactly lines startLine..endLine, ready to commit>}.",
|
|
1331
|
+
"Include \"suggestion\" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement source for every line in the range and nothing else — never prose describing the change, which belongs in \"body\"."
|
|
1082
1332
|
].join("\n");
|
|
1083
1333
|
};
|
|
1084
1334
|
const fileReviewerInstructions = makeFileReviewerInstructions();
|
|
1085
1335
|
const FileReviewToolkit = Toolkit.empty;
|
|
1086
|
-
/** Compatibility export: the evidence-only child has no handler requirements. */
|
|
1087
|
-
const FileReviewToolkitLayer = Layer.empty;
|
|
1088
1336
|
const defaultFileReviewerPolicy = AgentPolicy.make({
|
|
1089
1337
|
maxTurns: 6,
|
|
1090
1338
|
maxToolCalls: 1,
|
|
@@ -1096,54 +1344,29 @@ const defaultFileReviewerPolicy = AgentPolicy.make({
|
|
|
1096
1344
|
toolResultBounds: ToolResultBounds.make({ maxBytes: REVIEW_TOOL_RESULT_MAX_BYTES }),
|
|
1097
1345
|
onExhaustion: "fail"
|
|
1098
1346
|
});
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
});
|
|
1111
|
-
const sameStrings = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1112
|
-
const rejectWork = (workId, reason) => FileReviewWorkRejected.make({
|
|
1113
|
-
workId,
|
|
1114
|
-
reason
|
|
1115
|
-
});
|
|
1116
|
-
/** Validate coordinator scheduling against the current deterministic plan. */
|
|
1117
|
-
const prepareReviewBrief = (request) => Effect.gen(function* () {
|
|
1118
|
-
const source = yield* PullRequestSource;
|
|
1119
|
-
const mapSourceFailure = (failure) => rejectWork(request.workId, `pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600));
|
|
1120
|
-
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
1121
|
-
const plan = planReviewUnits(files, { totalChangedFiles: (yield* source.metadata.pipe(Effect.mapError(mapSourceFailure))).totalChangedFiles });
|
|
1122
|
-
const unit = plan.units.find((candidate) => candidate.unitId === request.unitId);
|
|
1123
|
-
if (unit === void 0 || !sameStrings(request.paths, unit.paths) || !sameStrings(request.evidenceShardIds, unit.evidenceShards.map((shard) => shard.shardId))) return yield* rejectWork(request.workId, "request does not match a host-planned unit");
|
|
1124
|
-
if (request.phase === "discovery") {
|
|
1125
|
-
const pass = plan.discoveryPasses.find((candidate) => candidate.passId === request.workId);
|
|
1126
|
-
if (pass === void 0 || pass.unitId !== request.unitId || !sameStrings(pass.paths, request.paths) || !sameStrings(pass.evidenceShardIds, request.evidenceShardIds) || pass.perspective !== request.perspective || !sameStrings(pass.riskCategories, request.riskCategories) || request.candidates.length !== 0) return yield* rejectWork(request.workId, "discovery request does not match the host plan");
|
|
1127
|
-
} else {
|
|
1128
|
-
if (request.workId !== `${request.unitId}-verification` || request.perspective !== "candidate-verification" || !sameStrings(request.riskCategories, unit.riskCategories) || request.candidates.length === 0) return yield* rejectWork(request.workId, "verification request does not match the host-planned unit");
|
|
1129
|
-
const candidateIds = /* @__PURE__ */ new Set();
|
|
1130
|
-
const candidateSubjects = /* @__PURE__ */ new Set();
|
|
1131
|
-
const allowed = new Set(unit.paths);
|
|
1132
|
-
for (const candidate of request.candidates) {
|
|
1133
|
-
const subjectKey = reviewCandidateSubjectKey(candidate);
|
|
1134
|
-
if (candidateIds.has(candidate.candidateId) || candidateSubjects.has(subjectKey) || candidate.unitId !== unit.unitId || candidate.evidencePaths.some((path) => !allowed.has(path)) || candidate._tag === "FindingCandidate" && !allowed.has(candidate.finding.path)) return yield* rejectWork(request.workId, "verification candidates are duplicated or outside the planned unit");
|
|
1135
|
-
candidateIds.add(candidate.candidateId);
|
|
1136
|
-
candidateSubjects.add(subjectKey);
|
|
1137
|
-
}
|
|
1347
|
+
const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-review-worker", {
|
|
1348
|
+
input: FileReviewBrief,
|
|
1349
|
+
output: FileReviewReport,
|
|
1350
|
+
instructions: makeFileReviewerInstructions(options),
|
|
1351
|
+
toolkit: FileReviewToolkit,
|
|
1352
|
+
policy: defaultFileReviewerPolicy,
|
|
1353
|
+
description: "Perform one bounded discovery or independent candidate-verification pass over host-supplied pull-request evidence.",
|
|
1354
|
+
metadata: {
|
|
1355
|
+
deploymentClass: "E",
|
|
1356
|
+
surface: "read-only",
|
|
1357
|
+
stage: "discovery-verification"
|
|
1138
1358
|
}
|
|
1359
|
+
});
|
|
1360
|
+
const FileReviewer = makeFileReviewerDefinition();
|
|
1361
|
+
const candidateOrdinal = (index) => String(index + 1).padStart(3, "0");
|
|
1362
|
+
/** Rebuild one unit's complete evidence from the same snapshot the plan used. */
|
|
1363
|
+
const unitEvidence = (unit, files) => Effect.gen(function* () {
|
|
1139
1364
|
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1140
1365
|
const evidence = [];
|
|
1141
1366
|
for (const shard of unit.evidenceShards) {
|
|
1142
1367
|
const file = byPath.get(shard.path);
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const chunk = chunks[shard.ordinal - 1];
|
|
1146
|
-
if (chunk === void 0 || chunks.length !== shard.total || chunk.annotatedPatch.length !== shard.evidenceChars) return yield* rejectWork(request.workId, `planned evidence shard no longer matches source: ${shard.shardId}`);
|
|
1368
|
+
const chunk = file === void 0 ? void 0 : fileReviewEvidenceChunks(file)[shard.ordinal - 1];
|
|
1369
|
+
if (file === void 0 || chunk === void 0) return yield* Effect.die(/* @__PURE__ */ new Error(`planned evidence shard has no source: ${shard.shardId} (${shard.path})`));
|
|
1147
1370
|
evidence.push(FileReviewEvidence.make({
|
|
1148
1371
|
shardId: shard.shardId,
|
|
1149
1372
|
path: shard.path,
|
|
@@ -1154,169 +1377,274 @@ const prepareReviewBrief = (request) => Effect.gen(function* () {
|
|
|
1154
1377
|
annotatedPatch: chunk.annotatedPatch
|
|
1155
1378
|
}));
|
|
1156
1379
|
}
|
|
1157
|
-
return
|
|
1158
|
-
...request,
|
|
1159
|
-
evidence
|
|
1160
|
-
});
|
|
1380
|
+
return evidence;
|
|
1161
1381
|
});
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1382
|
+
const misbehaved = (workId, reason) => ReviewPassMisbehaved.make({
|
|
1383
|
+
workId,
|
|
1384
|
+
reason: reason.slice(0, 600)
|
|
1385
|
+
});
|
|
1386
|
+
/** Validate that a verification report assesses exactly the scheduled candidates. */
|
|
1387
|
+
const validateVerificationReport = (brief, report) => {
|
|
1388
|
+
if (report.findings.length > 0 || report.concerns.length > 0 || report.fileSummaries.length > 0) return misbehaved(brief.workId, "verification output contained discovery-only fields");
|
|
1389
|
+
const expectedById = new Map(brief.candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
1390
|
+
const assessedIds = /* @__PURE__ */ new Set();
|
|
1391
|
+
for (const assessment of report.assessments) {
|
|
1392
|
+
const candidate = expectedById.get(assessment.candidateId);
|
|
1393
|
+
if (candidate === void 0 || assessedIds.has(assessment.candidateId)) return misbehaved(brief.workId, "verification output did not assess the exact candidate set");
|
|
1394
|
+
if (!assessmentSettlesSuggestionExactly(assessment, candidate)) return misbehaved(brief.workId, "verification output did not settle suggestion publication exactly");
|
|
1395
|
+
assessedIds.add(assessment.candidateId);
|
|
1396
|
+
}
|
|
1397
|
+
if (assessedIds.size !== expectedById.size) return misbehaved(brief.workId, "verification output did not assess the exact candidate set");
|
|
1398
|
+
};
|
|
1399
|
+
/**
|
|
1400
|
+
* Run one scheduled pass: execute the child, decode its report, and enforce
|
|
1401
|
+
* the pass contract. Any typed fault — child failure, malformed or misdirected
|
|
1402
|
+
* output — is retried once; budget exhaustion is terminal because a retry
|
|
1403
|
+
* would fail the same way. The settled outcome is a value either way, so one
|
|
1404
|
+
* flaky pass can never fail the whole pipeline.
|
|
1405
|
+
*/
|
|
1406
|
+
const runReviewPass = (binding, brief, budget) => Effect.gen(function* () {
|
|
1407
|
+
const result = yield* AgentRuntime.run(binding, brief, {
|
|
1408
|
+
...budget === void 0 ? {} : { budget },
|
|
1409
|
+
estimateCostMicrousd: () => Effect.succeed(500)
|
|
1410
|
+
});
|
|
1411
|
+
const report = yield* Schema.decodeUnknownEffect(FileReviewReport)(result.output).pipe(Effect.mapError((error) => misbehaved(brief.workId, `child report failed to decode: ${error.message}`)));
|
|
1412
|
+
if (report.phase !== brief.phase || report.workId !== brief.workId || report.unitId !== brief.unitId) return yield* misbehaved(brief.workId, "child report identity does not match the scheduled pass");
|
|
1413
|
+
if (brief.phase === "verification") {
|
|
1414
|
+
const violation = validateVerificationReport(brief, report);
|
|
1415
|
+
if (violation !== void 0) return yield* violation;
|
|
1416
|
+
} else if (report.assessments.length > 0) return yield* misbehaved(brief.workId, "discovery output contained verification-only assessments");
|
|
1417
|
+
return {
|
|
1418
|
+
report,
|
|
1419
|
+
turns: result.turns
|
|
1420
|
+
};
|
|
1421
|
+
}).pipe(Effect.scoped, Effect.retry({
|
|
1422
|
+
times: 1,
|
|
1423
|
+
while: (error) => error._tag !== "BudgetExceeded"
|
|
1424
|
+
}), Effect.map((settled) => ({
|
|
1425
|
+
_tag: "settled",
|
|
1426
|
+
...settled
|
|
1427
|
+
})), Effect.catch((error) => Effect.succeed({
|
|
1428
|
+
_tag: "failed",
|
|
1429
|
+
errorTag: String(error._tag).slice(0, 256)
|
|
1430
|
+
})));
|
|
1431
|
+
/**
|
|
1432
|
+
* Keep only findings anchored inside the pass's exact assigned evidence and
|
|
1433
|
+
* concerns bound to unit paths. Everything else is discarded and counted —
|
|
1434
|
+
* an invalid anchor invalidates one claim, never the pass that produced it.
|
|
1435
|
+
*/
|
|
1436
|
+
const harvestDiscovery = (pass, unit, files, anchorFiles, report) => {
|
|
1437
|
+
const allowed = new Set(pass.paths);
|
|
1438
|
+
let discarded = 0;
|
|
1439
|
+
const keptFindings = [];
|
|
1440
|
+
for (const finding of report.findings) {
|
|
1441
|
+
if (!allowed.has(finding.path) || anchorViolation(finding, anchorFiles) !== void 0 || !findingAnchorInUnitEvidence(finding, unit, files)) {
|
|
1442
|
+
discarded += 1;
|
|
1443
|
+
continue;
|
|
1173
1444
|
}
|
|
1174
|
-
|
|
1175
|
-
return Effect.succeed(FileReviewUnitResult.make({
|
|
1176
|
-
phase: report.phase,
|
|
1177
|
-
workId: report.workId,
|
|
1178
|
-
unitId: report.unitId,
|
|
1179
|
-
candidates: [],
|
|
1180
|
-
fileSummaries: [],
|
|
1181
|
-
assessments: report.assessments
|
|
1182
|
-
}));
|
|
1445
|
+
keptFindings.push(finding);
|
|
1183
1446
|
}
|
|
1184
|
-
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
const mapSourceFailure = (failure) => rejectWork(request.workId, `pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600));
|
|
1190
|
-
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
1191
|
-
const metadata = yield* source.metadata.pipe(Effect.mapError(mapSourceFailure));
|
|
1192
|
-
const anchorFiles = yield* source.anchorFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
1193
|
-
const unit = planReviewUnits(files, { totalChangedFiles: metadata.totalChangedFiles }).units.find((candidate) => candidate.unitId === request.unitId);
|
|
1194
|
-
if (unit === void 0) return yield* rejectWork(request.workId, "scheduled review unit is no longer available");
|
|
1195
|
-
for (const finding of report.findings) {
|
|
1196
|
-
const violation = anchorViolation(finding, anchorFiles);
|
|
1197
|
-
if (violation !== void 0 || !findingAnchorInUnitEvidence(finding, unit, files)) return yield* rejectWork(request.workId, `discovery finding has no valid anchor in its assigned evidence: ${violation ?? finding.path}`);
|
|
1447
|
+
const keptConcerns = [];
|
|
1448
|
+
for (const candidate of report.concerns) {
|
|
1449
|
+
if (candidate.evidencePaths.some((path) => !allowed.has(path))) {
|
|
1450
|
+
discarded += 1;
|
|
1451
|
+
continue;
|
|
1198
1452
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1453
|
+
keptConcerns.push(candidate);
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
candidates: [...keptFindings.map((finding, index) => FindingCandidate.make({
|
|
1457
|
+
candidateId: `${pass.passId}:finding:${candidateOrdinal(index)}`,
|
|
1458
|
+
workId: pass.passId,
|
|
1459
|
+
unitId: pass.unitId,
|
|
1203
1460
|
finding,
|
|
1204
1461
|
evidencePaths: [finding.path]
|
|
1205
|
-
}))
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
unitId: request.unitId,
|
|
1462
|
+
})), ...keptConcerns.map((candidate, index) => ConcernCandidate.make({
|
|
1463
|
+
candidateId: `${pass.passId}:concern:${candidateOrdinal(index)}`,
|
|
1464
|
+
workId: pass.passId,
|
|
1465
|
+
unitId: pass.unitId,
|
|
1210
1466
|
concern: candidate.concern,
|
|
1211
1467
|
evidencePaths: candidate.evidencePaths
|
|
1212
|
-
}))
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
unitId: report.unitId,
|
|
1217
|
-
candidates: [...findingCandidates, ...concernCandidates],
|
|
1218
|
-
fileSummaries: report.fileSummaries,
|
|
1219
|
-
assessments: []
|
|
1220
|
-
});
|
|
1221
|
-
});
|
|
1222
|
-
};
|
|
1223
|
-
const delegationDescription = "Run exactly one host-planned discovery or candidate-verification child. Copy every plan field and candidate verbatim; never retry failed work.";
|
|
1224
|
-
const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_review", {
|
|
1225
|
-
description: delegationDescription,
|
|
1226
|
-
target: child,
|
|
1227
|
-
parameters: FileReviewRequest,
|
|
1228
|
-
success: FileReviewUnitResult,
|
|
1229
|
-
failure: FileReviewFailure,
|
|
1230
|
-
failureMode: "return",
|
|
1231
|
-
prepareInput: prepareReviewBrief,
|
|
1232
|
-
projectResult: projectReviewResult,
|
|
1233
|
-
policy: fileReviewPolicy
|
|
1234
|
-
});
|
|
1235
|
-
var ListReviewUnitsQuery = class extends Schema.Class("@effect-agent/pr-review/ListReviewUnitsQuery")({ scope: Schema.Literal("all") }) {};
|
|
1236
|
-
const ListReviewUnits = Tool.make("list_review_units", {
|
|
1237
|
-
description: "List deterministic bounded review units, explicit risk categories, every required discovery pass, and paths the pipeline cannot cover.",
|
|
1238
|
-
parameters: ListReviewUnitsQuery,
|
|
1239
|
-
success: ReviewUnitPlan,
|
|
1240
|
-
failure: PullRequestSourceFailure,
|
|
1241
|
-
failureMode: "error",
|
|
1242
|
-
dependencies: [PullRequestSource]
|
|
1243
|
-
}).annotate(ToolExecutionClass, "readonly");
|
|
1244
|
-
const FanOutCoordinatorToolkit = Toolkit.make(ListReviewUnits);
|
|
1245
|
-
const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({ list_review_units: () => Effect.gen(function* () {
|
|
1246
|
-
const source = yield* PullRequestSource;
|
|
1247
|
-
return planReviewUnits(yield* source.changedFiles, { totalChangedFiles: (yield* source.metadata).totalChangedFiles });
|
|
1248
|
-
}) });
|
|
1249
|
-
const makeFanOutReviewInstructions = (options = {}) => (mission) => {
|
|
1250
|
-
const maxFindings = clampMaxFindings(options.maxFindings);
|
|
1251
|
-
return [
|
|
1252
|
-
`You coordinate the bounded multi-pass review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}.`,
|
|
1253
|
-
mission.body.length > 0 ? `Author description:\n${mission.body}` : "No author description.",
|
|
1254
|
-
...staticGuidanceLines(options.guidance),
|
|
1255
|
-
"1. Call list_review_units exactly once.",
|
|
1256
|
-
"2. For EVERY discoveryPass, call delegate_file_review exactly once with phase \"discovery\", workId=passId, and the pass unitId/paths/evidenceShardIds/perspective/riskCategories verbatim; candidates must be []. Prefer one bounded parallel batch. Never retry.",
|
|
1257
|
-
"3. Group candidates returned by all successful discovery passes by unit. Deterministically deduplicate byte-identical finding or concern payloads, retaining the first candidate in discoveryPass plan order. For every unit with at least one retained candidate, call delegate_file_review exactly once with phase \"verification\", workId \"<unitId>-verification\", perspective \"candidate-verification\", the unit paths/evidenceShardIds/riskCategories, and EVERY retained candidate copied byte-for-byte. Prefer one bounded parallel batch. Never retry.",
|
|
1258
|
-
"4. Verification is authoritative: rejected candidates must not be reported. The host independently reconstructs publishable findings from exact confirmed assessments, so do not select, rewrite, downgrade, or invent findings.",
|
|
1259
|
-
`5. Return ONLY CodeReview JSON. Write a concise summary of completed and failed stages. Set findings=[] and concerns=[]; the host injects exact confirmed candidates. Copy factual fileSummaries into walkthrough without invention. The host publication cap is ${maxFindings}.`,
|
|
1260
|
-
"No configured pipeline can prove absence of defects. Describe settled work, never an exhaustive or defect-free review."
|
|
1261
|
-
].join("\n");
|
|
1468
|
+
}))],
|
|
1469
|
+
fileSummaries: report.fileSummaries.filter((entry) => allowed.has(entry.path)),
|
|
1470
|
+
discarded
|
|
1471
|
+
};
|
|
1262
1472
|
};
|
|
1263
|
-
const
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1473
|
+
const reviewUnit = (binding, unit, passes, input) => Effect.gen(function* () {
|
|
1474
|
+
const evidence = yield* unitEvidence(unit, input.files);
|
|
1475
|
+
const failedPasses = [];
|
|
1476
|
+
const candidates = [];
|
|
1477
|
+
const subjects = /* @__PURE__ */ new Set();
|
|
1478
|
+
const walkthrough = [];
|
|
1479
|
+
let discardedFindings = 0;
|
|
1480
|
+
let turns = 0;
|
|
1481
|
+
let completedGeneralPasses = 0;
|
|
1482
|
+
let completedSpecialistPasses = 0;
|
|
1483
|
+
for (const pass of passes) {
|
|
1484
|
+
const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
|
|
1485
|
+
const brief = FileReviewBrief.make({
|
|
1486
|
+
phase: "discovery",
|
|
1487
|
+
workId: pass.passId,
|
|
1488
|
+
unitId: pass.unitId,
|
|
1489
|
+
paths: pass.paths,
|
|
1490
|
+
evidenceShardIds: pass.evidenceShardIds,
|
|
1491
|
+
perspective: pass.perspective,
|
|
1492
|
+
riskCategories: pass.riskCategories,
|
|
1493
|
+
candidates: [],
|
|
1494
|
+
evidence
|
|
1495
|
+
});
|
|
1496
|
+
const outcome = yield* runReviewPass(binding, brief, input.budget);
|
|
1497
|
+
if (outcome._tag === "failed") {
|
|
1498
|
+
failedPasses.push(FailedReviewPass.make({
|
|
1499
|
+
workId: pass.passId,
|
|
1500
|
+
stage,
|
|
1501
|
+
errorTag: outcome.errorTag
|
|
1502
|
+
}));
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
turns += outcome.turns;
|
|
1506
|
+
if (stage === "specialist") completedSpecialistPasses += 1;
|
|
1507
|
+
else completedGeneralPasses += 1;
|
|
1508
|
+
const harvest = harvestDiscovery(pass, unit, input.files, input.anchorFiles, outcome.report);
|
|
1509
|
+
discardedFindings += harvest.discarded;
|
|
1510
|
+
if (pass.perspective === "general") walkthrough.push(...harvest.fileSummaries);
|
|
1511
|
+
for (const candidate of harvest.candidates) {
|
|
1512
|
+
const subject = reviewCandidateSubjectKey(candidate);
|
|
1513
|
+
if (subjects.has(subject)) continue;
|
|
1514
|
+
subjects.add(subject);
|
|
1515
|
+
candidates.push(candidate);
|
|
1516
|
+
}
|
|
1285
1517
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1518
|
+
const confirmed = [];
|
|
1519
|
+
let rejectedCandidates = 0;
|
|
1520
|
+
let unsettledCandidates = 0;
|
|
1521
|
+
let completedVerificationPasses = 0;
|
|
1522
|
+
const requiredVerificationPasses = candidates.length > 0 ? 1 : 0;
|
|
1523
|
+
if (candidates.length > 0) {
|
|
1524
|
+
const workId = `${unit.unitId}-verification`;
|
|
1525
|
+
const brief = FileReviewBrief.make({
|
|
1526
|
+
phase: "verification",
|
|
1527
|
+
workId,
|
|
1528
|
+
unitId: unit.unitId,
|
|
1529
|
+
paths: unit.paths,
|
|
1530
|
+
evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
|
|
1531
|
+
perspective: "candidate-verification",
|
|
1532
|
+
riskCategories: unit.riskCategories,
|
|
1533
|
+
candidates,
|
|
1534
|
+
evidence
|
|
1535
|
+
});
|
|
1536
|
+
const outcome = yield* runReviewPass(binding, brief, input.budget);
|
|
1537
|
+
if (outcome._tag === "failed") {
|
|
1538
|
+
unsettledCandidates = candidates.length;
|
|
1539
|
+
failedPasses.push(FailedReviewPass.make({
|
|
1540
|
+
workId,
|
|
1541
|
+
stage: "verification",
|
|
1542
|
+
errorTag: outcome.errorTag
|
|
1543
|
+
}));
|
|
1544
|
+
} else {
|
|
1545
|
+
turns += outcome.turns;
|
|
1546
|
+
completedVerificationPasses = 1;
|
|
1547
|
+
const byId = new Map(candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
1548
|
+
for (const assessment of outcome.report.assessments) {
|
|
1549
|
+
const candidate = byId.get(assessment.candidateId);
|
|
1550
|
+
if (candidate === void 0) continue;
|
|
1551
|
+
if (assessment.disposition === "confirmed") confirmed.push({
|
|
1552
|
+
assessment,
|
|
1553
|
+
candidate
|
|
1554
|
+
});
|
|
1555
|
+
else rejectedCandidates += 1;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1300
1558
|
}
|
|
1301
|
-
});
|
|
1302
|
-
const makeFanOutReviewSuite = (options = {}) => {
|
|
1303
|
-
const child = makeFileReviewerDefinition({ guidance: options.guidance });
|
|
1304
|
-
const delegation = makeFileReviewDelegation(child);
|
|
1305
1559
|
return {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1560
|
+
failedPasses,
|
|
1561
|
+
discoveredCandidates: candidates.length,
|
|
1562
|
+
confirmed,
|
|
1563
|
+
rejectedCandidates,
|
|
1564
|
+
unsettledCandidates,
|
|
1565
|
+
discardedFindings,
|
|
1566
|
+
walkthrough,
|
|
1567
|
+
turns,
|
|
1568
|
+
completedGeneralPasses,
|
|
1569
|
+
completedSpecialistPasses,
|
|
1570
|
+
requiredVerificationPasses,
|
|
1571
|
+
completedVerificationPasses,
|
|
1572
|
+
unreviewedPaths: failedPasses.length > 0 ? unit.paths : []
|
|
1309
1573
|
};
|
|
1574
|
+
});
|
|
1575
|
+
const countNoun = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
1576
|
+
const composeSummary = (plan, assurance) => {
|
|
1577
|
+
const requiredDiscovery = assurance.requiredGeneralDiscoveryPasses + assurance.requiredSpecialistPasses;
|
|
1578
|
+
const completedDiscovery = assurance.completedGeneralDiscoveryPasses + assurance.completedSpecialistPasses;
|
|
1579
|
+
const parts = [`Reviewed ${countNoun(plan.totalFiles, "changed file")} across ${countNoun(plan.units.length, "bounded unit")}: ${completedDiscovery}/${requiredDiscovery} discovery and ${assurance.completedVerificationPasses}/${assurance.requiredVerificationPasses} verification pass(es) settled; ${assurance.confirmedCandidates} of ${countNoun(assurance.discoveredCandidates, "discovered candidate")} confirmed by independent verification.`];
|
|
1580
|
+
if (assurance.failedPasses.length > 0) parts.push(`${countNoun(assurance.failedPasses.length, "pass")} did not settle; the affected paths are carried forward and retried on the next run. This is a reviewer-side gap, not a code defect.`);
|
|
1581
|
+
if (assurance.discardedInvalidFindings > 0) parts.push(`${countNoun(assurance.discardedInvalidFindings, "candidate")} discarded for anchors or paths outside the assigned evidence.`);
|
|
1582
|
+
if (plan.undiffablePaths.length > 0) parts.push(`${countNoun(plan.undiffablePaths.length, "path")} had no reviewable textual evidence and keep input coverage incomplete; exclude such paths with ignore globs when that is intended.`);
|
|
1583
|
+
if (plan.unassignedPaths.length > 0 || plan.unassignedEvidenceShardCount > 0) parts.push("The changeset exceeded the bounded fan-out capacity; unassigned scope is reported under input coverage.");
|
|
1584
|
+
parts.push("No configured pipeline can prove absence of defects; this describes settled work only.");
|
|
1585
|
+
return parts.join(" ").slice(0, 4e3);
|
|
1310
1586
|
};
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1587
|
+
/**
|
|
1588
|
+
* Run the complete host-scheduled fan-out pipeline over one selected
|
|
1589
|
+
* changeset snapshot: plan, independent discovery, exact verification, and a
|
|
1590
|
+
* deterministic host-composed CodeReview from verifier-confirmed candidates
|
|
1591
|
+
* only. The verdict is derived from confirmed severities, never model prose.
|
|
1592
|
+
*/
|
|
1593
|
+
const runFanOutReview = (binding, input) => Effect.gen(function* () {
|
|
1594
|
+
const plan = planReviewUnits(input.files, { totalChangedFiles: input.totalChangedFiles });
|
|
1595
|
+
const passesByUnit = /* @__PURE__ */ new Map();
|
|
1596
|
+
for (const pass of plan.discoveryPasses) {
|
|
1597
|
+
const passes = passesByUnit.get(pass.unitId) ?? [];
|
|
1598
|
+
passes.push(pass);
|
|
1599
|
+
passesByUnit.set(pass.unitId, passes);
|
|
1600
|
+
}
|
|
1601
|
+
const outcomes = yield* Effect.forEach(plan.units, (unit) => reviewUnit(binding, unit, passesByUnit.get(unit.unitId) ?? [], input), { concurrency: 4 });
|
|
1602
|
+
const failedPasses = outcomes.flatMap((outcome) => outcome.failedPasses);
|
|
1603
|
+
const unsettledCandidates = outcomes.reduce((total, outcome) => total + outcome.unsettledCandidates, 0);
|
|
1604
|
+
const reasons = [];
|
|
1605
|
+
if (failedPasses.length > 0) reasons.push(boundedListReason("configured review passes did not settle", failedPasses.map((pass) => `${pass.workId} (${pass.errorTag})`)));
|
|
1606
|
+
if (unsettledCandidates > 0) reasons.push(`${unsettledCandidates} discovered candidate(s) did not receive exact verification`);
|
|
1607
|
+
const requiredSpecialistPasses = plan.discoveryPasses.filter((pass) => pass.perspective === "risk-specialist").length;
|
|
1608
|
+
const confirmed = outcomes.flatMap((outcome) => outcome.confirmed);
|
|
1609
|
+
const assurance = ReviewAssurance.make({
|
|
1610
|
+
status: reasons.length === 0 ? "settled" : "incomplete",
|
|
1611
|
+
requiredGeneralDiscoveryPasses: plan.discoveryPasses.length - requiredSpecialistPasses,
|
|
1612
|
+
completedGeneralDiscoveryPasses: outcomes.reduce((total, outcome) => total + outcome.completedGeneralPasses, 0),
|
|
1613
|
+
requiredSpecialistPasses,
|
|
1614
|
+
completedSpecialistPasses: outcomes.reduce((total, outcome) => total + outcome.completedSpecialistPasses, 0),
|
|
1615
|
+
requiredVerificationPasses: outcomes.reduce((total, outcome) => total + outcome.requiredVerificationPasses, 0),
|
|
1616
|
+
completedVerificationPasses: outcomes.reduce((total, outcome) => total + outcome.completedVerificationPasses, 0),
|
|
1617
|
+
discoveredCandidates: outcomes.reduce((total, outcome) => total + outcome.discoveredCandidates, 0),
|
|
1618
|
+
confirmedCandidates: confirmed.length,
|
|
1619
|
+
rejectedCandidates: outcomes.reduce((total, outcome) => total + outcome.rejectedCandidates, 0),
|
|
1620
|
+
unsettledCandidates,
|
|
1621
|
+
discardedInvalidFindings: outcomes.reduce((total, outcome) => total + outcome.discardedFindings, 0),
|
|
1622
|
+
failedPasses,
|
|
1623
|
+
reasons
|
|
1624
|
+
});
|
|
1625
|
+
const findings = rankAndDedupeFindings(confirmed.flatMap(({ assessment, candidate }) => candidate._tag === "FindingCandidate" ? [confirmedFindingForPublication(assessment, candidate)] : []));
|
|
1626
|
+
const concerns = rankAndDedupeConcerns(confirmed.flatMap(({ candidate }) => candidate._tag === "ConcernCandidate" ? [candidate.concern] : []));
|
|
1627
|
+
const walkthrough = outcomes.flatMap((outcome) => outcome.walkthrough);
|
|
1628
|
+
const blocking = findings.some((finding) => finding.severity === "blocking") || concerns.some((concern) => concern.severity === "blocking");
|
|
1629
|
+
return {
|
|
1630
|
+
review: CodeReview.make({
|
|
1631
|
+
summary: composeSummary(plan, assurance),
|
|
1632
|
+
verdict: blocking ? "request-changes" : findings.length > 0 || concerns.length > 0 ? "comment" : "approve",
|
|
1633
|
+
findings,
|
|
1634
|
+
...concerns.length === 0 ? {} : { concerns },
|
|
1635
|
+
...walkthrough.length === 0 ? {} : { walkthrough }
|
|
1636
|
+
}),
|
|
1637
|
+
assurance,
|
|
1638
|
+
plan,
|
|
1639
|
+
unreviewedPaths: [.../* @__PURE__ */ new Set([
|
|
1640
|
+
...outcomes.flatMap((outcome) => outcome.unreviewedPaths),
|
|
1641
|
+
...plan.unassignedPaths,
|
|
1642
|
+
...plan.partialEvidencePaths,
|
|
1643
|
+
...plan.undiffablePaths
|
|
1644
|
+
])].sort(),
|
|
1645
|
+
turns: outcomes.reduce((total, outcome) => total + outcome.turns, 0)
|
|
1646
|
+
};
|
|
1647
|
+
});
|
|
1320
1648
|
//#endregion
|
|
1321
1649
|
//#region src/internal/fingerprint.ts
|
|
1322
1650
|
const MARKER_PREFIX = "<!-- effect-agent-pr-review fingerprint=sha256:";
|
|
@@ -1381,16 +1709,21 @@ var StoredReviewConcern = class extends Schema.Class("@effect-agent/pr-review/St
|
|
|
1381
1709
|
title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
|
|
1382
1710
|
body: StoredText
|
|
1383
1711
|
}) {};
|
|
1712
|
+
/** The carried-scope bound; a run that cannot fit its leftovers publishes no state. */
|
|
1713
|
+
const MAX_STORED_UNREVIEWED_PATHS = 100;
|
|
1384
1714
|
/**
|
|
1385
|
-
* Versioned state embedded
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1715
|
+
* Versioned state embedded after EVERY completed run that can be signed. The
|
|
1716
|
+
* head plus full-scope fingerprint forms an incremental baseline; an absent
|
|
1717
|
+
* unresolved item never means the path is defect-free. `unreviewedPaths`
|
|
1718
|
+
* carries retryable review gaps (failed passes) forward so the next
|
|
1719
|
+
* incremental run re-reviews exactly them plus the new delta — the baseline
|
|
1720
|
+
* advances monotonically instead of freezing on one flaky pass and reopening
|
|
1721
|
+
* the whole post-baseline scope. The `acceptedScopeFingerprint` name is
|
|
1722
|
+
* retained for wire compatibility. Storing hundreds of path strings
|
|
1723
|
+
* separately would not fit GitHub's bounded review body in the worst case.
|
|
1391
1724
|
*/
|
|
1392
1725
|
var ReviewState = class extends Schema.Class("@effect-agent/pr-review/ReviewState")({
|
|
1393
|
-
version: Schema.Literal(
|
|
1726
|
+
version: Schema.Literal(2),
|
|
1394
1727
|
repository: Schema.NonEmptyString.check(Schema.isMaxLength(200)),
|
|
1395
1728
|
pullRequestNumber: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
1396
1729
|
baseRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
@@ -1405,6 +1738,14 @@ var ReviewState = class extends Schema.Class("@effect-agent/pr-review/ReviewStat
|
|
|
1405
1738
|
})),
|
|
1406
1739
|
unresolvedFindings: Schema.Array(StoredReviewFinding).check(Schema.isMaxLength(20)),
|
|
1407
1740
|
unresolvedConcerns: Schema.Array(StoredReviewConcern).check(Schema.isMaxLength(10)),
|
|
1741
|
+
/** Retryable review gaps carried into the next incremental run's scope. */
|
|
1742
|
+
unreviewedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(100)),
|
|
1743
|
+
/**
|
|
1744
|
+
* True only when the producing run had complete input coverage, no
|
|
1745
|
+
* unsettled pass, and nothing carried. Skip-unchanged authority: an
|
|
1746
|
+
* unchanged patch may skip re-review only over a settled state.
|
|
1747
|
+
*/
|
|
1748
|
+
settled: Schema.Boolean,
|
|
1408
1749
|
lastReviewMode: ReviewScopeMode
|
|
1409
1750
|
}) {};
|
|
1410
1751
|
const toStoredFinding = (finding) => StoredReviewFinding.make({
|
|
@@ -1433,12 +1774,12 @@ const fromStoredConcern = (concern) => ReviewConcern.make({
|
|
|
1433
1774
|
title: concern.title,
|
|
1434
1775
|
body: concern.body
|
|
1435
1776
|
});
|
|
1436
|
-
const STATE_MARKER_PREFIX = "<!-- effect-agent-pr-review state-
|
|
1777
|
+
const STATE_MARKER_PREFIX = "<!-- effect-agent-pr-review state-v2:";
|
|
1437
1778
|
const STATE_MARKER_SUFFIX = " -->";
|
|
1438
|
-
const STATE_MARKER_PATTERN = /(?:^|\n)<!-- effect-agent-pr-review state-
|
|
1439
|
-
const STATE_SIGNATURE_DOMAIN = "effect-agent-pr-review/state-
|
|
1779
|
+
const STATE_MARKER_PATTERN = /(?:^|\n)<!-- effect-agent-pr-review state-v2:([A-Za-z0-9+/]+={0,2})\.([0-9a-f]{64}) -->$/;
|
|
1780
|
+
const STATE_SIGNATURE_DOMAIN = "effect-agent-pr-review/state-v2\0";
|
|
1440
1781
|
const MAX_REVIEW_STATE_MARKER_CHARS = 24e3;
|
|
1441
|
-
const ReviewStateMarker = Schema.NonEmptyString.check(Schema.isMaxLength(MAX_REVIEW_STATE_MARKER_CHARS), Schema.isPattern(/^<!-- effect-agent-pr-review state-
|
|
1782
|
+
const ReviewStateMarker = Schema.NonEmptyString.check(Schema.isMaxLength(MAX_REVIEW_STATE_MARKER_CHARS), Schema.isPattern(/^<!-- effect-agent-pr-review state-v2:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->$/)).pipe(Schema.brand("@effect-agent/pr-review/ReviewStateMarker"));
|
|
1442
1783
|
var ReviewStateAuthenticationFailure = class extends Schema.TaggedError()("ReviewStateAuthenticationFailure", {
|
|
1443
1784
|
operation: Schema.Literals(["sign", "verify"]),
|
|
1444
1785
|
reason: Schema.NonEmptyString.check(Schema.isMaxLength(2048))
|
|
@@ -1591,13 +1932,17 @@ const selectReviewRange = (input) => {
|
|
|
1591
1932
|
const currentPaths = new Set(input.fullFiles.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]));
|
|
1592
1933
|
const selectedByPath = /* @__PURE__ */ new Map();
|
|
1593
1934
|
for (const file of comparison.files) if (currentPaths.has(file.path) || file.previousPath !== void 0 && currentPaths.has(file.previousPath)) selectedByPath.set(file.path, file);
|
|
1594
|
-
|
|
1595
|
-
|
|
1935
|
+
const carriedPaths = new Set(input.priorState.unreviewedPaths.filter((path) => currentPaths.has(path)));
|
|
1936
|
+
for (const path of carriedPaths) affectedPaths.add(path);
|
|
1937
|
+
const rescuePaths = input.priorState.baseSha !== input.current.baseSha;
|
|
1938
|
+
if (rescuePaths || carriedPaths.size > 0) {
|
|
1939
|
+
for (const file of input.fullFiles) if (rescuePaths && (affectedPaths.has(file.path) || file.previousPath !== void 0 && affectedPaths.has(file.previousPath)) || carriedPaths.has(file.path) || file.previousPath !== void 0 && carriedPaths.has(file.previousPath)) selectedByPath.set(file.path, file);
|
|
1596
1940
|
}
|
|
1597
1941
|
const selectedFiles = [...selectedByPath.values()].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1942
|
+
const carriedReason = carriedPaths.size === 0 ? "" : `; retrying ${carriedPaths.size} carried unreviewed path(s)`;
|
|
1598
1943
|
return {
|
|
1599
1944
|
mode: "incremental",
|
|
1600
|
-
reason: `changes since
|
|
1945
|
+
reason: `changes since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}${carriedReason}`,
|
|
1601
1946
|
files: selectedFiles,
|
|
1602
1947
|
affectedPaths: [...affectedPaths].sort(),
|
|
1603
1948
|
totalFiles: selectedFiles.length,
|
|
@@ -1696,7 +2041,7 @@ var ReviewRetirementReport = class extends Schema.Class("@effect-agent/pr-review
|
|
|
1696
2041
|
const findingIdentity = (finding) => `${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.title}`;
|
|
1697
2042
|
const REVIEW_METADATA_PATTERN = /<!-- effect-agent-pr-review metadata\n[\s\S]*?\n-->/g;
|
|
1698
2043
|
const FINGERPRINT_PATTERN = /<!-- effect-agent-pr-review fingerprint=sha256:[0-9a-f]{64} -->/g;
|
|
1699
|
-
const STATE_PATTERN = /<!-- effect-agent-pr-review state-
|
|
2044
|
+
const STATE_PATTERN = /<!-- effect-agent-pr-review state-v\d+:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->/g;
|
|
1700
2045
|
const RETIRED_ORIGINAL_PATTERN = /<!-- effect-agent-pr-review retired-original:start -->\n([\s\S]*?)\n<!-- effect-agent-pr-review retired-original:end -->/;
|
|
1701
2046
|
const MACHINE_COMMENT_PATTERN = new RegExp(`${REVIEW_METADATA_PATTERN.source}|${FINGERPRINT_PATTERN.source}|${STATE_PATTERN.source}`, "g");
|
|
1702
2047
|
const VERDICT_CALLOUT_PATTERN = /^(?:> \[!(?:CAUTION|IMPORTANT)\]\n> [^\n]*(?:\n> [^\n]*)*|> (?:ℹ️|✅)[^\n]*)\n*/;
|
|
@@ -2269,6 +2614,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
|
|
|
2269
2614
|
return Option.isSome(latest) && latest.value === current;
|
|
2270
2615
|
});
|
|
2271
2616
|
//#endregion
|
|
2272
|
-
export { DiscoveredConcern as $,
|
|
2617
|
+
export { DiscoveredConcern as $, commentableLines as $n, boundedListReason as $t, ReviewStateAuthenticationFailure as A, clampMaxFindings as An, MAX_UNIT_FILES as At, selectReviewRange as B, MAX_CHANGED_FILES as Bn, UNIT_CHANGED_LINE_BUDGET as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, ReviewConcern as Cn, reviewCandidateSubjectKey as Ct, ReviewMode as D, ReviewToolkitLayer as Dn, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as Dt, ReviewHeadComparison as E, ReviewToolkit as En, MAX_MERGED_FINDINGS as Et, StoredReviewFinding as F, makeReviewInstructions as Fn, ReviewPassId as Ft, validateReviewState as G, ReviewInputViolation as Gn, rankAndDedupeConcerns as Gt, toStoredConcern as H, PullRequestMetadata as Hn, classifyReviewRisks as Ht, buildProfileMission as I, readFileDiffHandler as In, ReviewRiskCategory as It, computeChangesetFingerprint as J, ChangedFile as Jn, FailedReviewUnit as Jt, webCryptoReviewStateAuthenticatorLayer as K, normalizeRepoRelativePath as Kn, rankAndDedupeFindings as Kt, computeProfileFingerprint as L, readFileHandler as Ln, ReviewUnit as Lt, ReviewStateMarker as M, fileDiffView as Mn, ReviewDiscoveryPerspective as Mt, ReviewStateMarkerTooLarge as N, fileReviewEvidenceChunks as Nn, ReviewEvidenceShard as Nt, ReviewScopeMode as O, ReviewVerdict as On, MAX_REVIEW_UNITS as Ot, StoredReviewConcern as P, listChangedFilesHandler as Pn, ReviewEvidenceShardId as Pt, ConcernCandidate as Q, annotatePatch as Qn, assessFlatReview as Qt, fromStoredConcern as R, resolveGuidance as Rn, ReviewUnitId as Rt, GitCommitSha as S, ReadFileDiff as Sn, makeFileReviewerInstructions as St, ReviewExecutionContext as T, ReviewMission as Tn, MAX_FILE_EVIDENCE_CHARS as Tt, toStoredFinding as U, PullRequestSource as Un, findingAnchorInUnitEvidence as Ut, selectedPullRequestSourceLayer as V, MAX_FILE_CHARS as Vn, UNIT_EVIDENCE_CHAR_BUDGET as Vt, unavailableReviewStateAuthenticatorLayer as W, PullRequestSourceFailure as Wn, planReviewUnits as Wt, renderFingerprintMarker as X, ChangedPath as Xn, ReviewCoverage as Xt, extractFingerprint as Y, ChangedFileStatus as Yn, ReviewAssurance as Yt, CandidateAssessment as Z, MAX_REVIEW_CONTENT_CHARS as Zn, ReviewInputCoverage as Zt, ReviewRetirementHost as _, MAX_WALKTHROUGH_ENTRIES as _n, assessmentSettlesSuggestionExactly as _t, PriorReviews as a, CodeReview as an, FindingCandidate as at, hasReviewMetadataMarker as b, REVIEW_TOOL_RESULT_MAX_BYTES as bn, fileReviewerInstructions as bt, fingerprintUnchanged as c, FileSlice as cn, MAX_FILE_REVIEW_TOOL_CALLS as ct, gitHubReviewPublisherLayer as d, FindingSeverity as dn, REVIEW_UNIT_CONCURRENCY as dt, compatibilityCoverage as en, hasReviewableContent as er, FileReviewBrief as et, gitHubReviewRetirementHostLayer as f, ListChangedFiles as fn, ReviewCandidate as ft, ReviewRetirementFailure as g, MAX_PATCH_CHARS as gn, ReviewWorkPhase as gt, RetirableReviewComment as h, MAX_FINDINGS as hn, ReviewWorkPerspective as ht, PriorReviewLookupFailure as i, ChangedFilesView as in, FileReviewer as it, ReviewStateAuthenticator as j, defaultReviewPolicy as jn, ReviewDiscoveryPass as jt, ReviewState as k, WalkthroughEntry as kn, MAX_UNIT_EVIDENCE_SHARDS as kt, gitHubPriorReviewsLayer as l, FileSliceQuery as ln, MAX_REVIEW_CHILDREN as lt, RetirableReview as m, MAX_CONCERNS as mn, ReviewPassMisbehaved as mt, GitHubApiFailure as n, flatAssurance as nn, parsePatch as nr, FileReviewReport as nt, PublishedReview as o, FileDiffQuery as on, MAX_CHILD_CONCERNS as ot, parseGitHubSubmittedAt as p, ListChangedFilesQuery as pn, ReviewCandidateId as pt, FINGERPRINT_MARKER_LENGTH as q, anchorViolation as qn, FailedReviewPass as qt, GitHubReviewTarget as r, ChangedFileSummary as rn, renderReviewContent as rr, FileReviewToolkit as rt, ReviewPublisher as s, FileDiffView as sn, MAX_CHILD_FINDINGS as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, fanOutInputCoverage as tn, isReviewableFile as tr, FileReviewEvidence as tt, gitHubPullRequestSourceLayer as u, FindingCategory as un, MAX_UNIT_CANDIDATES as ut, ReviewRetirementReport as v, MAX_WALKTHROUGH_SUMMARY_CHARS as vn, confirmedFindingForPublication as vt, MAX_STORED_UNREVIEWED_PATHS as w, ReviewFinding as wn, runFanOutReview as wt, retireStaleReviews as x, ReadFile as xn, makeFileReviewerDefinition as xt, decideReviewRetirement as y, PullRequestReviewer as yn, defaultFileReviewerPolicy as yt, fromStoredFinding as z, reviewInstructions as zn, ReviewUnitPlan as zt };
|
|
2273
2618
|
|
|
2274
|
-
//# sourceMappingURL=github-
|
|
2619
|
+
//# sourceMappingURL=github-BbwYzNrC.mjs.map
|