@effect-agent/pr-review 0.1.0-beta.20 → 0.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -26
- package/dist/action.d.mts +10 -4
- package/dist/action.mjs +17 -27
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-CQFA-o0v.d.mts → fan-out-D5xrmadQ.d.mts} +288 -223
- package/dist/{github-DnenG3be.mjs → github-DSqZp3Ce.mjs} +622 -195
- package/dist/github-DSqZp3Ce.mjs.map +1 -0
- package/dist/index.d.mts +24 -51
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BE83_Tfo.mjs → providers-CblG1G9b.mjs} +403 -134
- package/dist/providers-CblG1G9b.mjs.map +1 -0
- package/dist/testing.d.mts +10 -47
- package/dist/testing.mjs +29 -55
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -39
- package/src/index.ts +1 -0
- package/src/internal/anchors.ts +20 -0
- package/src/internal/coverage.ts +612 -134
- package/src/internal/factory.ts +12 -9
- package/src/internal/fan-out-scripted.ts +61 -94
- package/src/internal/fan-out.ts +505 -256
- package/src/internal/profiles.ts +12 -0
- package/src/internal/progress.ts +1 -1
- package/src/internal/render.ts +51 -25
- package/src/internal/review-agent.ts +68 -24
- package/src/internal/review-state.ts +7 -5
- package/src/internal/review-units.ts +326 -32
- package/src/internal/run.ts +48 -41
- package/dist/github-DnenG3be.mjs.map +0 -1
- package/dist/providers-BE83_Tfo.mjs.map +0 -1
|
@@ -169,6 +169,18 @@ const annotatePatch = (patch) => {
|
|
|
169
169
|
return output.join("\n");
|
|
170
170
|
};
|
|
171
171
|
//#endregion
|
|
172
|
+
//#region src/internal/anchors.ts
|
|
173
|
+
/** Why a finding cannot anchor to the current new-version diff, if any. */
|
|
174
|
+
const anchorViolation = (finding, files) => {
|
|
175
|
+
const file = files.find((candidate) => candidate.path === finding.path);
|
|
176
|
+
if (file === void 0) return "path is not part of the changeset";
|
|
177
|
+
if (file.patch === void 0) return "file has no anchorable textual diff";
|
|
178
|
+
if (finding.endLine < finding.startLine) return "endLine precedes startLine";
|
|
179
|
+
if (finding.endLine - finding.startLine + 1 > 100) return "range is implausibly large";
|
|
180
|
+
const anchors = commentableLines(file.patch);
|
|
181
|
+
for (let line = finding.startLine; line <= finding.endLine; line += 1) if (!anchors.has(line)) return `line ${line} is not part of the diff`;
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
172
184
|
//#region src/internal/source.ts
|
|
173
185
|
/** Reading a file head version larger than this is refused, never truncated silently. */
|
|
174
186
|
const MAX_FILE_CHARS = 2e5;
|
|
@@ -235,7 +247,7 @@ var PullRequestSource = class extends Context.Service()("@effect-agent/pr-review
|
|
|
235
247
|
const MAX_FINDINGS = 20;
|
|
236
248
|
/** The hard non-anchored-concerns bound carried by the CodeReview schema. */
|
|
237
249
|
const MAX_CONCERNS = 10;
|
|
238
|
-
/**
|
|
250
|
+
/** Maximum characters in one deterministic model-visible evidence chunk. */
|
|
239
251
|
const MAX_PATCH_CHARS = 6e4;
|
|
240
252
|
/** The encoded Tool result must retain one complete bounded content fallback. */
|
|
241
253
|
const REVIEW_TOOL_RESULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
@@ -287,6 +299,53 @@ var FileDiffView = class extends Schema.Class("@effect-agent/pr-review/FileDiffV
|
|
|
287
299
|
annotatedPatch: Schema.String,
|
|
288
300
|
truncated: Schema.Boolean
|
|
289
301
|
}) {};
|
|
302
|
+
/**
|
|
303
|
+
* Split complete model-visible evidence at deterministic line boundaries.
|
|
304
|
+
* A pathological single line is hard-sliced so every character is still
|
|
305
|
+
* assigned and every chunk remains within the provider-independent bound.
|
|
306
|
+
*/
|
|
307
|
+
const boundedEvidenceChunks = (evidence) => {
|
|
308
|
+
if (evidence.length <= 6e4) return [evidence];
|
|
309
|
+
const chunks = [];
|
|
310
|
+
let offset = 0;
|
|
311
|
+
while (offset < evidence.length) {
|
|
312
|
+
let end = Math.min(offset + MAX_PATCH_CHARS, evidence.length);
|
|
313
|
+
if (end < evidence.length) {
|
|
314
|
+
const boundary = evidence.lastIndexOf("\n", end - 1);
|
|
315
|
+
if (boundary >= offset) end = boundary + 1;
|
|
316
|
+
}
|
|
317
|
+
if (end === offset) end = Math.min(offset + MAX_PATCH_CHARS, evidence.length);
|
|
318
|
+
chunks.push(evidence.slice(offset, end));
|
|
319
|
+
offset = end;
|
|
320
|
+
}
|
|
321
|
+
return chunks;
|
|
322
|
+
};
|
|
323
|
+
/** Complete bounded evidence chunks used by deterministic fan-out planning. */
|
|
324
|
+
const fileReviewEvidenceChunks = (file) => {
|
|
325
|
+
const contentEvidence = renderReviewContent(file);
|
|
326
|
+
const reviewMode = file.patch !== void 0 ? "diff" : contentEvidence !== void 0 ? "content" : "unavailable";
|
|
327
|
+
const annotated = file.patch === void 0 ? contentEvidence ?? "" : annotatePatch(file.patch);
|
|
328
|
+
return boundedEvidenceChunks(annotated).map((annotatedPatch) => ({
|
|
329
|
+
reviewMode,
|
|
330
|
+
annotatedPatch
|
|
331
|
+
}));
|
|
332
|
+
};
|
|
333
|
+
/** Host-owned rendering of one changed file's bounded review evidence. */
|
|
334
|
+
const fileDiffView = (file) => {
|
|
335
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
336
|
+
const first = chunks[0] ?? {
|
|
337
|
+
reviewMode: "unavailable",
|
|
338
|
+
annotatedPatch: ""
|
|
339
|
+
};
|
|
340
|
+
const truncated = first.reviewMode === "diff" && chunks.length > 1;
|
|
341
|
+
return FileDiffView.make({
|
|
342
|
+
path: file.path,
|
|
343
|
+
status: file.status,
|
|
344
|
+
reviewMode: first.reviewMode,
|
|
345
|
+
annotatedPatch: truncated ? `${first.annotatedPatch}\n[diff truncated]` : first.annotatedPatch,
|
|
346
|
+
truncated
|
|
347
|
+
});
|
|
348
|
+
};
|
|
290
349
|
const ReadFileDiff = Tool.make("read_file_diff", {
|
|
291
350
|
description: "Read one changed file's review evidence. A normal unified diff marks valid anchors as R<number>. When GitHub omitted the diff, bounded base/head content is returned with B/H line labels for review but no valid inline anchors.",
|
|
292
351
|
parameters: FileDiffQuery,
|
|
@@ -352,17 +411,7 @@ const readFileDiffHandler = (query) => Effect.gen(function* () {
|
|
|
352
411
|
input: relative,
|
|
353
412
|
reason: "Path is not part of this pull request's changeset."
|
|
354
413
|
});
|
|
355
|
-
|
|
356
|
-
const reviewMode = file.patch !== void 0 ? "diff" : contentEvidence !== void 0 ? "content" : "unavailable";
|
|
357
|
-
const annotated = file.patch === void 0 ? contentEvidence ?? "" : annotatePatch(file.patch);
|
|
358
|
-
const truncated = reviewMode === "diff" && annotated.length > MAX_PATCH_CHARS;
|
|
359
|
-
return FileDiffView.make({
|
|
360
|
-
path: file.path,
|
|
361
|
-
status: file.status,
|
|
362
|
-
reviewMode,
|
|
363
|
-
annotatedPatch: truncated ? `${annotated.slice(0, MAX_PATCH_CHARS)}\n[diff truncated]` : annotated,
|
|
364
|
-
truncated
|
|
365
|
-
});
|
|
414
|
+
return fileDiffView(file);
|
|
366
415
|
});
|
|
367
416
|
/**
|
|
368
417
|
* The `read_file` handler, shared verbatim by the flat reviewer's toolkit
|
|
@@ -489,15 +538,15 @@ const makeReviewInstructions = (options = {}) => (mission) => {
|
|
|
489
538
|
mission.body.length > 0 ? `Author description:\n${mission.body}` : "The author provided no description.",
|
|
490
539
|
...resolveGuidance(options.guidance, mission),
|
|
491
540
|
"Work in this order:",
|
|
492
|
-
"1. Call list_changed_files once to see the
|
|
493
|
-
"2. Call read_file_diff for every file
|
|
541
|
+
"1. Call list_changed_files once to see the selected input scope. In incremental reviews it is deliberately a subset of the pull request's full diff (totalFiles counts the whole pull request); omitted paths belong to settled prior scope or explicit host exclusions, not to this run.",
|
|
542
|
+
"2. Call read_file_diff for every listed file. A normal diff marks new-version anchors as R<number>; only those numbers are valid startLine/endLine values. When GitHub omitted a diff, the tool may return bounded base/head content marked B/H instead. Review that content, but report its defects as non-anchored concerns because B/H lines cannot anchor GitHub comments. Never anchor a finding to a removed (-), B, or H line.",
|
|
494
543
|
"3. Call read_file when you need surrounding context the diff does not show. ONLY listed files are readable — read_file_diff and read_file both return a failed result for any other path (an import, a neighbor, a file named in the description). Do not request or retry unlisted paths; reason from the visible diffs instead and note the gap honestly in your summary when it matters.",
|
|
495
544
|
"4. Review for real defects first: correctness, security, concurrency, resource leaks, error handling, API misuse. Style nits are least important. Do not praise; do not restate the diff.",
|
|
496
545
|
"When the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. The usual tell is a loose assertion standing where an exact one belongs — >= or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed.",
|
|
497
546
|
"Go shallow only when the diff has no behavioral surface at all: doc typos, formatting, lockfile or generated-code regeneration, a mechanical rename. Line count is not the signal — a one-line change to auth, money, SQL, a comparison operator, or a config default is not trivial.",
|
|
498
547
|
"Drop bloat-shaped findings before reporting: defensive checks for cases that cannot happen, abstractions used once, comments restating obvious code, tests asserting tautologies, just-in-case guards. A finding must be sound, correct, and worth acting on; prefer an explicit keep over an invented finding.",
|
|
499
548
|
"5. After collecting anchored findings, deliberately scan for concerns with NO line to point at: deletion or cleanup plans for code the diff replaces, rollout or migration sequencing, coverage gaps the diff implies but does not add, scope questions only the author can answer. Report each as a \"concern\", never as a finding with an invented anchor; report none when none exist.",
|
|
500
|
-
`6. Write a walkthrough: for every file you
|
|
549
|
+
`6. Write a walkthrough: for every file whose evidence you examined, one factual sentence (<= 240 chars) describing what changed in that file — written for a reader scanning the pull request, never restating the diff line by line. Use only paths from list_changed_files; invented paths are dropped.`,
|
|
501
550
|
"7. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {\"summary\": <string, 1-3 paragraphs of overall assessment>, \"verdict\": <\"approve\" | \"comment\" | \"request-changes\">, \"findings\": [{\"path\": <string, a changed file path>, \"startLine\": <integer, an R-marked new-file line>, \"endLine\": <integer, >= startLine, same file, R-marked>, \"severity\": <\"blocking\" | \"important\" | \"nit\">, \"category\": <OPTIONAL: \"correctness\" | \"security\" | \"concurrency\" | \"performance\" | \"resources\" | \"error-handling\" | \"testing\" | \"maintainability\" | \"style\" | \"docs\">, \"title\": <string, <= 120 chars>, \"body\": <string, why it matters and what to do>, \"suggestion\": <string, OPTIONAL: replacement text for exactly lines startLine..endLine, ready to commit>}], \"concerns\": <array, OPTIONAL: [{\"severity\": <\"blocking\" | \"important\" | \"nit\">, \"title\": <string, <= 120 chars>, \"body\": <string>}], only for step-5 concerns with no valid anchor — never duplicate a finding here>, \"walkthrough\": <array, OPTIONAL: [{\"path\": <string, a changed file path>, \"summary\": <string, the step-6 sentence>}], one entry per reviewed file>}.",
|
|
502
551
|
`Report at most ${maxFindings} findings and at most 10 concerns; prefer the most important ones. An empty findings array with verdict "approve" is a valid review. Include "suggestion" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement for every line in the range and nothing else.`,
|
|
503
552
|
"Use verdict \"request-changes\" only when at least one finding or concern is \"blocking\". Line anchors you invent will be discarded, so copy R-numbers from read_file_diff output."
|
|
@@ -535,19 +584,70 @@ const PullRequestReviewer = Agent.define("pr-reviewer", {
|
|
|
535
584
|
const MAX_REVIEW_UNITS = 8;
|
|
536
585
|
/** A unit never carries more files than this, regardless of their size. */
|
|
537
586
|
const MAX_UNIT_FILES = 12;
|
|
538
|
-
/**
|
|
587
|
+
/** Compatibility export; complete evidence chars now own unit packing. */
|
|
539
588
|
const UNIT_CHANGED_LINE_BUDGET = 800;
|
|
540
|
-
/**
|
|
541
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Bound the complete model-visible evidence assigned to one child. This is a
|
|
591
|
+
* character bound rather than a token estimate because it is deterministic,
|
|
592
|
+
* provider-independent, and enforced before any model call.
|
|
593
|
+
*/
|
|
594
|
+
const UNIT_EVIDENCE_CHAR_BUDGET = 24e4;
|
|
595
|
+
/** Maximum complete evidence shards placed in one child brief. */
|
|
596
|
+
const MAX_UNIT_EVIDENCE_SHARDS = 12;
|
|
597
|
+
/**
|
|
598
|
+
* Keep overflow diagnostics bounded to one plan's total assignment capacity.
|
|
599
|
+
* The plan separately records the exact overflow count and every affected
|
|
600
|
+
* path, so identifiers are a deterministic diagnostic sample rather than the
|
|
601
|
+
* authority for whether input coverage is complete.
|
|
602
|
+
*/
|
|
603
|
+
const MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS = 96;
|
|
604
|
+
/** @deprecated Use `MAX_PATCH_CHARS`; this is now the per-shard bound. */
|
|
605
|
+
const MAX_FILE_EVIDENCE_CHARS = MAX_PATCH_CHARS;
|
|
542
606
|
/** The merged review never exceeds the `CodeReview` findings bound. */
|
|
543
607
|
const MAX_MERGED_FINDINGS = 20;
|
|
544
608
|
const ReviewUnitId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
|
|
609
|
+
/** High-risk surfaces that receive an explicit specialist focus label. */
|
|
610
|
+
const ReviewRiskCategory = Schema.Literals([
|
|
611
|
+
"authentication-authorization",
|
|
612
|
+
"security-boundary",
|
|
613
|
+
"persistence-durability",
|
|
614
|
+
"concurrency",
|
|
615
|
+
"credential-handling",
|
|
616
|
+
"external-side-effects"
|
|
617
|
+
]);
|
|
618
|
+
const ReviewDiscoveryPerspective = Schema.Literals(["general", "risk-specialist"]);
|
|
619
|
+
const ReviewPassId = Schema.NonEmptyString.check(Schema.isMaxLength(64));
|
|
620
|
+
const ReviewEvidenceShardId = Schema.NonEmptyString.check(Schema.isMaxLength(32));
|
|
621
|
+
/** One complete bounded slice of a changed path's model-visible evidence. */
|
|
622
|
+
var ReviewEvidenceShard = class extends Schema.Class("@effect-agent/pr-review/ReviewEvidenceShard")({
|
|
623
|
+
shardId: ReviewEvidenceShardId,
|
|
624
|
+
path: ChangedPath,
|
|
625
|
+
ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
626
|
+
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
627
|
+
evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(MAX_PATCH_CHARS))
|
|
628
|
+
}) {};
|
|
629
|
+
const EvidenceShardIds$1 = Schema.Array(ReviewEvidenceShardId).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
|
|
630
|
+
/** One required, independently scoped discovery attempt. */
|
|
631
|
+
var ReviewDiscoveryPass = class extends Schema.Class("@effect-agent/pr-review/ReviewDiscoveryPass")({
|
|
632
|
+
passId: ReviewPassId,
|
|
633
|
+
unitId: ReviewUnitId,
|
|
634
|
+
paths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12)),
|
|
635
|
+
evidenceShardIds: EvidenceShardIds$1,
|
|
636
|
+
perspective: ReviewDiscoveryPerspective,
|
|
637
|
+
/** Empty for the general pass; explicit deterministic focus for specialists. */
|
|
638
|
+
riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6))
|
|
639
|
+
}) {};
|
|
545
640
|
/** One bounded slice of the changeset delegated to one child reviewer. */
|
|
546
641
|
var ReviewUnit = class extends Schema.Class("@effect-agent/pr-review/ReviewUnit")({
|
|
547
642
|
unitId: ReviewUnitId,
|
|
548
643
|
paths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12)),
|
|
644
|
+
evidenceShards: Schema.Array(ReviewEvidenceShard).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12)),
|
|
549
645
|
/** additions + deletions across the unit's files, for honest sizing. */
|
|
550
|
-
changedLines: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
646
|
+
changedLines: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
647
|
+
/** Complete model-visible diff/content evidence assigned to each child. */
|
|
648
|
+
evidenceChars: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(UNIT_EVIDENCE_CHAR_BUDGET)),
|
|
649
|
+
/** Host-classified focus labels for the unit's redundant specialist pass. */
|
|
650
|
+
riskCategories: Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6))
|
|
551
651
|
}) {};
|
|
552
652
|
/** The complete deterministic fan-out plan over one changeset. */
|
|
553
653
|
var ReviewUnitPlan = class extends Schema.Class("@effect-agent/pr-review/ReviewUnitPlan")({
|
|
@@ -555,8 +655,16 @@ var ReviewUnitPlan = class extends Schema.Class("@effect-agent/pr-review/ReviewU
|
|
|
555
655
|
/** True when the source returned fewer files than the pull request has. */
|
|
556
656
|
truncated: Schema.Boolean,
|
|
557
657
|
units: Schema.Array(ReviewUnit).check(Schema.isMaxLength(8)),
|
|
658
|
+
/** Exact discovery calls the coordinator must make. */
|
|
659
|
+
discoveryPasses: Schema.Array(ReviewDiscoveryPass).check(Schema.isMaxLength(16)),
|
|
558
660
|
/** Changed files with neither a textual diff nor bounded base/head text. */
|
|
559
661
|
undiffablePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
|
|
662
|
+
/** Assigned paths with one or more evidence shards beyond plan capacity. */
|
|
663
|
+
partialEvidencePaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300)),
|
|
664
|
+
/** Exact number of shards beyond the bounded unit capacity. */
|
|
665
|
+
unassignedEvidenceShardCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
666
|
+
/** Bounded deterministic prefix of the unassigned shard identifiers. */
|
|
667
|
+
unassignedEvidenceShardIds: Schema.Array(ReviewEvidenceShardId).check(Schema.isMaxLength(96)),
|
|
560
668
|
/**
|
|
561
669
|
* Diffable files beyond the fan-out capacity (MAX_REVIEW_UNITS units of
|
|
562
670
|
* MAX_UNIT_FILES files). Never silently dropped: the coordinator must name
|
|
@@ -564,62 +672,232 @@ var ReviewUnitPlan = class extends Schema.Class("@effect-agent/pr-review/ReviewU
|
|
|
564
672
|
*/
|
|
565
673
|
unassignedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(300))
|
|
566
674
|
}) {};
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
675
|
+
const riskRules = [
|
|
676
|
+
{
|
|
677
|
+
category: "authentication-authorization",
|
|
678
|
+
patterns: [
|
|
679
|
+
/auth/,
|
|
680
|
+
/authoriz/,
|
|
681
|
+
/permission/,
|
|
682
|
+
/principal/,
|
|
683
|
+
/access[-_ ]?control/,
|
|
684
|
+
/role\b/
|
|
685
|
+
]
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
category: "security-boundary",
|
|
689
|
+
patterns: [
|
|
690
|
+
/security/,
|
|
691
|
+
/sandbox/,
|
|
692
|
+
/untrusted/,
|
|
693
|
+
/schema\.decode/,
|
|
694
|
+
/validation/,
|
|
695
|
+
/injection/,
|
|
696
|
+
/csrf/,
|
|
697
|
+
/xss/,
|
|
698
|
+
/path traversal/
|
|
699
|
+
]
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
category: "persistence-durability",
|
|
703
|
+
patterns: [
|
|
704
|
+
/durab/,
|
|
705
|
+
/persist/,
|
|
706
|
+
/storage/,
|
|
707
|
+
/database/,
|
|
708
|
+
/\bsql\b/,
|
|
709
|
+
/journal/,
|
|
710
|
+
/ledger/,
|
|
711
|
+
/checkpoint/,
|
|
712
|
+
/migration/,
|
|
713
|
+
/transaction/
|
|
714
|
+
]
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
category: "concurrency",
|
|
718
|
+
patterns: [
|
|
719
|
+
/concurr/,
|
|
720
|
+
/semaphore/,
|
|
721
|
+
/\bfiber/,
|
|
722
|
+
/race/,
|
|
723
|
+
/mutex/,
|
|
724
|
+
/\block\b/,
|
|
725
|
+
/queue/,
|
|
726
|
+
/parallel/,
|
|
727
|
+
/interrupt/
|
|
728
|
+
]
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
category: "credential-handling",
|
|
732
|
+
patterns: [
|
|
733
|
+
/credential/,
|
|
734
|
+
/secret/,
|
|
735
|
+
/password/,
|
|
736
|
+
/api[-_ ]?key/,
|
|
737
|
+
/bearer/,
|
|
738
|
+
/hmac/,
|
|
739
|
+
/signature/
|
|
740
|
+
]
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
category: "external-side-effects",
|
|
744
|
+
patterns: [
|
|
745
|
+
/publish/,
|
|
746
|
+
/webhook/,
|
|
747
|
+
/github/,
|
|
748
|
+
/fetch\(/,
|
|
749
|
+
/http/,
|
|
750
|
+
/send[-_ ]?(email|message)/,
|
|
751
|
+
/write[-_ ]?(file|record)/,
|
|
752
|
+
/delete/,
|
|
753
|
+
/mutation/,
|
|
754
|
+
/side[-_ ]?effect/,
|
|
755
|
+
/spawn/,
|
|
756
|
+
/exec/
|
|
757
|
+
]
|
|
758
|
+
}
|
|
759
|
+
];
|
|
760
|
+
/**
|
|
761
|
+
* Deterministic host policy for specialist assignment. It intentionally
|
|
762
|
+
* favors false positives: an extra bounded pass costs work, while a missed
|
|
763
|
+
* high-risk classification removes redundancy. This is not a claim that the
|
|
764
|
+
* keyword policy recognizes every semantically risky change.
|
|
765
|
+
*/
|
|
766
|
+
const classifyReviewRisks = (file) => {
|
|
767
|
+
const text = [
|
|
768
|
+
file.path,
|
|
769
|
+
file.previousPath ?? "",
|
|
770
|
+
file.patch ?? "",
|
|
771
|
+
file.reviewBaseContent ?? "",
|
|
772
|
+
file.reviewHeadContent ?? ""
|
|
773
|
+
].join("\n").toLowerCase();
|
|
774
|
+
return riskRules.filter((rule) => rule.patterns.some((pattern) => pattern.test(text))).map((rule) => rule.category);
|
|
775
|
+
};
|
|
776
|
+
/**
|
|
777
|
+
* Whether every claimed finding anchor was present in the exact bounded
|
|
778
|
+
* evidence shards assigned to one unit. This is stricter than checking the
|
|
779
|
+
* full pull-request diff when an oversized path spans multiple units.
|
|
780
|
+
*/
|
|
781
|
+
const findingAnchorInUnitEvidence = (finding, unit, files) => {
|
|
782
|
+
const file = files.find((candidate) => candidate.path === finding.path);
|
|
783
|
+
if (file?.patch === void 0 || finding.endLine < finding.startLine) return false;
|
|
784
|
+
const assignedOrdinals = new Set(unit.evidenceShards.filter((shard) => shard.path === finding.path).map((shard) => shard.ordinal));
|
|
785
|
+
const visibleLines = /* @__PURE__ */ new Set();
|
|
786
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
787
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
788
|
+
if (!assignedOrdinals.has(index + 1)) continue;
|
|
789
|
+
for (const line of chunks[index]?.annotatedPatch.split("\n") ?? []) {
|
|
790
|
+
const match = /^R(\d+) /.exec(line);
|
|
791
|
+
if (match?.[1] !== void 0) visibleLines.add(Number(match[1]));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
for (let line = finding.startLine; line <= finding.endLine; line += 1) if (!visibleLines.has(line)) return false;
|
|
795
|
+
return true;
|
|
796
|
+
};
|
|
797
|
+
const uniquePaths = (shards) => [...new Set(shards.map(({ shard }) => shard.path))];
|
|
798
|
+
const plannedEvidenceShards = (files) => {
|
|
799
|
+
const planned = [];
|
|
800
|
+
let shardIndex = 0;
|
|
801
|
+
for (const file of files) {
|
|
802
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
803
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
804
|
+
const chunk = chunks[index];
|
|
805
|
+
if (chunk === void 0) continue;
|
|
806
|
+
shardIndex += 1;
|
|
807
|
+
planned.push({
|
|
808
|
+
shard: ReviewEvidenceShard.make({
|
|
809
|
+
shardId: `shard-${String(shardIndex).padStart(4, "0")}`,
|
|
810
|
+
path: file.path,
|
|
811
|
+
ordinal: index + 1,
|
|
812
|
+
total: chunks.length,
|
|
813
|
+
evidenceChars: chunk.annotatedPatch.length
|
|
814
|
+
}),
|
|
815
|
+
file,
|
|
816
|
+
changedLines: index === 0 ? file.additions + file.deletions : 0
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return planned;
|
|
571
821
|
};
|
|
572
|
-
const unitOf = (index,
|
|
822
|
+
const unitOf = (index, shards) => ReviewUnit.make({
|
|
573
823
|
unitId: `unit-${String(index + 1).padStart(3, "0")}`,
|
|
574
|
-
paths:
|
|
575
|
-
|
|
824
|
+
paths: uniquePaths(shards),
|
|
825
|
+
evidenceShards: shards.map(({ shard }) => shard),
|
|
826
|
+
changedLines: shards.reduce((total, shard) => total + shard.changedLines, 0),
|
|
827
|
+
evidenceChars: shards.reduce((total, { shard }) => total + shard.evidenceChars, 0),
|
|
828
|
+
riskCategories: [...new Set(shards.flatMap(({ file }) => classifyReviewRisks(file)))]
|
|
576
829
|
});
|
|
830
|
+
const discoveryPassesFor = (units) => units.flatMap((unit) => [ReviewDiscoveryPass.make({
|
|
831
|
+
passId: `${unit.unitId}-general`,
|
|
832
|
+
unitId: unit.unitId,
|
|
833
|
+
paths: unit.paths,
|
|
834
|
+
evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
|
|
835
|
+
perspective: "general",
|
|
836
|
+
riskCategories: []
|
|
837
|
+
}), ReviewDiscoveryPass.make({
|
|
838
|
+
passId: `${unit.unitId}-specialist`,
|
|
839
|
+
unitId: unit.unitId,
|
|
840
|
+
paths: unit.paths,
|
|
841
|
+
evidenceShardIds: unit.evidenceShards.map((shard) => shard.shardId),
|
|
842
|
+
perspective: "risk-specialist",
|
|
843
|
+
riskCategories: unit.riskCategories
|
|
844
|
+
})]);
|
|
577
845
|
/**
|
|
578
846
|
* Group the changeset into at most `MAX_REVIEW_UNITS` review units.
|
|
579
847
|
*
|
|
580
848
|
* Deterministic by construction: files are ordered by path (so files sharing
|
|
581
849
|
* a directory become neighbors — directory affinity without a heuristic),
|
|
582
|
-
* then
|
|
583
|
-
* the hard per-unit
|
|
850
|
+
* then split into complete line-bounded evidence shards and packed greedily
|
|
851
|
+
* under the hard evidence and per-unit shard bounds. Capacity is finite and
|
|
852
|
+
* explicit:
|
|
584
853
|
*
|
|
585
854
|
* - files without a textual diff are still delegated when the source
|
|
586
855
|
* recovered complete bounded UTF-8 base/head content. Findings from that
|
|
587
856
|
* evidence cannot anchor inline and are reported as concerns;
|
|
588
857
|
* - files with neither form of textual evidence surface in
|
|
589
858
|
* `undiffablePaths` instead of laundering missing coverage;
|
|
590
|
-
* -
|
|
591
|
-
*
|
|
592
|
-
*
|
|
859
|
+
* - an oversized path spans as many deterministic shards and units as needed;
|
|
860
|
+
* - shards beyond `MAX_REVIEW_UNITS` full units surface explicitly, so a path
|
|
861
|
+
* is partial only when finite plan capacity is genuinely exhausted.
|
|
593
862
|
*/
|
|
594
863
|
const planReviewUnits = (files, options) => {
|
|
595
864
|
const ordered = [...files].sort((left, right) => left.path < right.path ? -1 : 1);
|
|
596
865
|
const reviewable = ordered.filter(isReviewableFile);
|
|
597
866
|
const undiffable = ordered.filter((file) => !isReviewableFile(file));
|
|
867
|
+
const shards = plannedEvidenceShards(reviewable);
|
|
598
868
|
const groups = [];
|
|
599
869
|
const unassigned = [];
|
|
600
870
|
let current = [];
|
|
601
|
-
let
|
|
602
|
-
for (const
|
|
603
|
-
const
|
|
604
|
-
if (current.length >= 12 || current.length > 0 &&
|
|
871
|
+
let currentEvidenceChars = 0;
|
|
872
|
+
for (const shard of shards) {
|
|
873
|
+
const nextPaths = /* @__PURE__ */ new Set([...uniquePaths(current), shard.shard.path]);
|
|
874
|
+
if (current.length >= 12 || nextPaths.size > 12 || current.length > 0 && currentEvidenceChars + shard.shard.evidenceChars > 24e4) {
|
|
605
875
|
groups.push(current);
|
|
606
876
|
current = [];
|
|
607
|
-
|
|
877
|
+
currentEvidenceChars = 0;
|
|
608
878
|
}
|
|
609
879
|
if (groups.length >= 8) {
|
|
610
|
-
unassigned.push(
|
|
880
|
+
unassigned.push(shard);
|
|
611
881
|
continue;
|
|
612
882
|
}
|
|
613
|
-
current.push(
|
|
614
|
-
|
|
883
|
+
current.push(shard);
|
|
884
|
+
currentEvidenceChars += shard.shard.evidenceChars;
|
|
615
885
|
}
|
|
616
886
|
if (current.length > 0 && groups.length < 8) groups.push(current);
|
|
887
|
+
const units = groups.map((group, index) => unitOf(index, group));
|
|
888
|
+
const assignedShardIds = new Set(units.flatMap((unit) => unit.evidenceShards.map((shard) => shard.shardId)));
|
|
889
|
+
const assignedPaths = new Set(shards.filter(({ shard }) => assignedShardIds.has(shard.shardId)).map(({ shard }) => shard.path));
|
|
890
|
+
const unassignedPathsWithEvidence = new Set(unassigned.map(({ shard }) => shard.path));
|
|
617
891
|
return ReviewUnitPlan.make({
|
|
618
892
|
totalFiles: files.length,
|
|
619
893
|
truncated: files.length < options.totalChangedFiles,
|
|
620
|
-
units
|
|
894
|
+
units,
|
|
895
|
+
discoveryPasses: discoveryPassesFor(units),
|
|
621
896
|
undiffablePaths: undiffable.map((file) => file.path),
|
|
622
|
-
|
|
897
|
+
partialEvidencePaths: [...unassignedPathsWithEvidence].filter((path) => assignedPaths.has(path)),
|
|
898
|
+
unassignedEvidenceShardCount: unassigned.length,
|
|
899
|
+
unassignedEvidenceShardIds: unassigned.slice(0, 96).map(({ shard }) => shard.shardId),
|
|
900
|
+
unassignedPaths: [...unassignedPathsWithEvidence].filter((path) => !assignedPaths.has(path))
|
|
623
901
|
});
|
|
624
902
|
};
|
|
625
903
|
const severityRank = {
|
|
@@ -652,116 +930,311 @@ const rankAndDedupeFindings = (findings) => {
|
|
|
652
930
|
};
|
|
653
931
|
//#endregion
|
|
654
932
|
//#region src/internal/fan-out.ts
|
|
655
|
-
/** One
|
|
656
|
-
const MAX_CHILD_FINDINGS =
|
|
657
|
-
/** One
|
|
933
|
+
/** One discovery pass returns at most this many anchored candidates. */
|
|
934
|
+
const MAX_CHILD_FINDINGS = 6;
|
|
935
|
+
/** One discovery pass returns at most this many non-anchored candidates. */
|
|
658
936
|
const MAX_CHILD_CONCERNS = 3;
|
|
937
|
+
/** Every unit receives independent general and specialist discovery passes. */
|
|
938
|
+
const MAX_UNIT_CANDIDATES = 18;
|
|
939
|
+
/** General + specialist discovery for every unit, then one verifier per unit. */
|
|
940
|
+
const MAX_REVIEW_CHILDREN = 24;
|
|
941
|
+
/** Structural minimum for a child that exposes no tools. */
|
|
942
|
+
const MAX_FILE_REVIEW_TOOL_CALLS = 1;
|
|
943
|
+
const ReviewWorkPhase = Schema.Literals(["discovery", "verification"]);
|
|
944
|
+
const ReviewWorkPerspective = Schema.Literals([
|
|
945
|
+
"general",
|
|
946
|
+
"risk-specialist",
|
|
947
|
+
"candidate-verification"
|
|
948
|
+
]);
|
|
949
|
+
const ReviewCandidateId = Schema.NonEmptyString.check(Schema.isMaxLength(96));
|
|
950
|
+
var FindingCandidate = class extends Schema.TaggedClass()("FindingCandidate", {
|
|
951
|
+
candidateId: ReviewCandidateId,
|
|
952
|
+
workId: ReviewPassId,
|
|
953
|
+
unitId: ReviewUnitId,
|
|
954
|
+
finding: ReviewFinding,
|
|
955
|
+
evidencePaths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(1))
|
|
956
|
+
}) {};
|
|
957
|
+
var ConcernCandidate = class extends Schema.TaggedClass()("ConcernCandidate", {
|
|
958
|
+
candidateId: ReviewCandidateId,
|
|
959
|
+
workId: ReviewPassId,
|
|
960
|
+
unitId: ReviewUnitId,
|
|
961
|
+
concern: ReviewConcern,
|
|
962
|
+
evidencePaths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(3))
|
|
963
|
+
}) {};
|
|
964
|
+
const ReviewCandidate = Schema.Union([FindingCandidate, ConcernCandidate]);
|
|
965
|
+
/** Deterministic host equivalence for claims repeated across discovery passes. */
|
|
966
|
+
const reviewCandidateSubjectKey = (candidate) => candidate._tag === "FindingCandidate" ? `finding:${JSON.stringify(Schema.encodeSync(ReviewFinding)(candidate.finding))}` : `concern:${JSON.stringify(Schema.encodeSync(ReviewConcern)(candidate.concern))}`;
|
|
967
|
+
var CandidateAssessment = class extends Schema.Class("@effect-agent/pr-review/CandidateAssessment")({
|
|
968
|
+
candidateId: ReviewCandidateId,
|
|
969
|
+
disposition: Schema.Literals(["confirmed", "rejected"]),
|
|
970
|
+
rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600))
|
|
971
|
+
}) {};
|
|
659
972
|
/**
|
|
660
|
-
*
|
|
661
|
-
*
|
|
973
|
+
* Concern candidates need explicit paths internally to bind the claim to
|
|
974
|
+
* scheduled evidence. The verifier receives the complete bounded unit so it
|
|
975
|
+
* can use neighboring evidence to falsify the claim. The public ReviewConcern
|
|
976
|
+
* remains path-free after the host confirms and projects it.
|
|
662
977
|
*/
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
read_file: readFileHandler
|
|
668
|
-
});
|
|
978
|
+
var DiscoveredConcern = class extends Schema.Class("@effect-agent/pr-review/DiscoveredConcern")({
|
|
979
|
+
concern: ReviewConcern,
|
|
980
|
+
evidencePaths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(3))
|
|
981
|
+
}) {};
|
|
669
982
|
const UnitPaths = Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
|
|
670
|
-
|
|
983
|
+
const RiskCategories = Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6));
|
|
984
|
+
const Candidates = Schema.Array(ReviewCandidate).check(Schema.isMaxLength(18));
|
|
985
|
+
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
|
+
/** One complete host-selected evidence shard supplied to a review child. */
|
|
999
|
+
var FileReviewEvidence = class extends Schema.Class("@effect-agent/pr-review/FileReviewEvidence")({
|
|
1000
|
+
shardId: ReviewEvidenceShardId,
|
|
1001
|
+
path: ChangedPath,
|
|
1002
|
+
status: ChangedFileStatus,
|
|
1003
|
+
reviewMode: Schema.Literals([
|
|
1004
|
+
"diff",
|
|
1005
|
+
"content",
|
|
1006
|
+
"unavailable"
|
|
1007
|
+
]),
|
|
1008
|
+
ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
1009
|
+
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
1010
|
+
annotatedPatch: Schema.String.check(Schema.isMaxLength(MAX_PATCH_CHARS))
|
|
1011
|
+
}) {};
|
|
1012
|
+
/** Host-prepared child input with complete bounded diff/content evidence. */
|
|
671
1013
|
var FileReviewBrief = class extends Schema.Class("@effect-agent/pr-review/FileReviewBrief")({
|
|
1014
|
+
phase: ReviewWorkPhase,
|
|
1015
|
+
workId: ReviewPassId,
|
|
672
1016
|
unitId: ReviewUnitId,
|
|
673
1017
|
paths: UnitPaths,
|
|
674
|
-
|
|
1018
|
+
evidenceShardIds: EvidenceShardIds,
|
|
1019
|
+
perspective: ReviewWorkPerspective,
|
|
1020
|
+
riskCategories: RiskCategories,
|
|
1021
|
+
candidates: Candidates,
|
|
1022
|
+
evidence: Schema.Array(FileReviewEvidence).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12))
|
|
675
1023
|
}) {};
|
|
676
|
-
/**
|
|
1024
|
+
/** Child output; phase-inapplicable collections must be empty. */
|
|
677
1025
|
var FileReviewReport = class extends Schema.Class("@effect-agent/pr-review/FileReviewReport")({
|
|
1026
|
+
phase: ReviewWorkPhase,
|
|
1027
|
+
workId: ReviewPassId,
|
|
678
1028
|
unitId: ReviewUnitId,
|
|
679
|
-
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1029
|
+
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(6)),
|
|
1030
|
+
concerns: Schema.Array(DiscoveredConcern).check(Schema.isMaxLength(3)),
|
|
1031
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)),
|
|
1032
|
+
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(18))
|
|
1033
|
+
}) {};
|
|
1034
|
+
/** Bounded coordinator-visible result with host-assigned candidate IDs. */
|
|
1035
|
+
var FileReviewUnitResult = class extends Schema.Class("@effect-agent/pr-review/FileReviewUnitResult")({
|
|
1036
|
+
phase: ReviewWorkPhase,
|
|
1037
|
+
workId: ReviewPassId,
|
|
1038
|
+
unitId: ReviewUnitId,
|
|
1039
|
+
candidates: Candidates,
|
|
1040
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)),
|
|
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", {
|
|
1048
|
+
workId: ReviewPassId,
|
|
1049
|
+
reason: Schema.NonEmptyString.check(Schema.isMaxLength(600))
|
|
684
1050
|
}) {};
|
|
1051
|
+
const FileReviewFailure = Schema.Union([FileReviewUnitFailed, FileReviewWorkRejected]);
|
|
685
1052
|
const staticGuidanceLines = (guidance) => {
|
|
686
1053
|
if (guidance === void 0) return [];
|
|
687
1054
|
return (typeof guidance === "string" ? [guidance] : guidance).filter((line) => line.length > 0);
|
|
688
1055
|
};
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1056
|
+
const evidenceInstructions = [
|
|
1057
|
+
"The host placed complete bounded review evidence shards in the input evidence array. Treat every shard as required input; ordinal/total identifies multi-shard paths.",
|
|
1058
|
+
"You have no tools and cannot roam outside this evidence. If it is insufficient for a candidate, reject or omit that candidate rather than guessing.",
|
|
1059
|
+
"A diff marks new-version anchors as R<number>; only those lines may anchor findings. B/H content evidence is non-anchorable."
|
|
1060
|
+
];
|
|
1061
|
+
/** Discovery and verification instructions share one child definition. */
|
|
1062
|
+
const makeFileReviewerInstructions = (options = {}) => (brief) => {
|
|
1063
|
+
const common = [
|
|
1064
|
+
`You are an attached review worker for ${brief.workId} in host-planned unit ${brief.unitId}: ${brief.paths.join(", ")}.`,
|
|
1065
|
+
...staticGuidanceLines(options.guidance),
|
|
1066
|
+
...evidenceInstructions
|
|
1067
|
+
];
|
|
1068
|
+
if (brief.phase === "verification") return [
|
|
1069
|
+
...common,
|
|
1070
|
+
"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
|
+
"The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
|
|
1072
|
+
"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."
|
|
1074
|
+
].join("\n");
|
|
1075
|
+
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
|
+
return [
|
|
1077
|
+
...common,
|
|
1078
|
+
focus,
|
|
1079
|
+
"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
|
+
"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.`
|
|
1082
|
+
].join("\n");
|
|
1083
|
+
};
|
|
703
1084
|
const fileReviewerInstructions = makeFileReviewerInstructions();
|
|
704
|
-
|
|
1085
|
+
const FileReviewToolkit = Toolkit.empty;
|
|
1086
|
+
/** Compatibility export: the evidence-only child has no handler requirements. */
|
|
1087
|
+
const FileReviewToolkitLayer = Layer.empty;
|
|
705
1088
|
const defaultFileReviewerPolicy = AgentPolicy.make({
|
|
706
|
-
maxTurns:
|
|
707
|
-
maxToolCalls:
|
|
1089
|
+
maxTurns: 6,
|
|
1090
|
+
maxToolCalls: 1,
|
|
708
1091
|
maxDuration: "6 minutes",
|
|
709
1092
|
toolConcurrency: 2,
|
|
710
|
-
repeatedFailureLimit:
|
|
1093
|
+
repeatedFailureLimit: 6,
|
|
711
1094
|
tokenBudget: 2e5,
|
|
712
1095
|
contextTokenLimit: 15e4,
|
|
713
1096
|
toolResultBounds: ToolResultBounds.make({ maxBytes: REVIEW_TOOL_RESULT_MAX_BYTES }),
|
|
714
1097
|
onExhaustion: "fail"
|
|
715
1098
|
});
|
|
716
|
-
/** The model-decoded delegation parameters: which unit to review. */
|
|
717
|
-
var FileReviewRequest = class extends Schema.Class("@effect-agent/pr-review/FileReviewRequest")({
|
|
718
|
-
unitId: ReviewUnitId,
|
|
719
|
-
paths: UnitPaths
|
|
720
|
-
}) {};
|
|
721
|
-
/** The bounded parent-visible result of one delegated unit review. */
|
|
722
|
-
var FileReviewUnitResult = class extends Schema.Class("@effect-agent/pr-review/FileReviewUnitResult")({
|
|
723
|
-
unitId: ReviewUnitId,
|
|
724
|
-
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(8)),
|
|
725
|
-
/** Unit-scoped concerns with no diff line to anchor to. */
|
|
726
|
-
concerns: Schema.optionalKey(Schema.Array(ReviewConcern).check(Schema.isMaxLength(3))),
|
|
727
|
-
/** One-sentence per-file change summaries for the merged walkthrough. */
|
|
728
|
-
fileSummaries: Schema.optionalKey(Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)))
|
|
729
|
-
}) {};
|
|
730
|
-
/**
|
|
731
|
-
* One unit's review failed: the child Run ended in a typed failure (policy
|
|
732
|
-
* bound, output violation, model fault). The marker is bounded and carries no
|
|
733
|
-
* child transcript content beyond the failure tag and message.
|
|
734
|
-
*/
|
|
735
|
-
var FileReviewUnitFailed = class extends Schema.TaggedError()("FileReviewUnitFailed", {
|
|
736
|
-
childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
737
|
-
message: Schema.String.check(Schema.isMaxLength(400))
|
|
738
|
-
}) {};
|
|
739
|
-
/**
|
|
740
|
-
* Finite per-invocation bounds (SUB-009), aligned with the child's own
|
|
741
|
-
* AgentPolicy: the child's policy is the limit that trips typed; the
|
|
742
|
-
* reservation mirrors it so parent-side accounting stays honest.
|
|
743
|
-
*/
|
|
744
1099
|
const fileReviewPolicy = SubagentPolicy.make({
|
|
745
|
-
maxChildren:
|
|
746
|
-
maxConcurrency:
|
|
747
|
-
maxTurns:
|
|
748
|
-
maxToolCalls:
|
|
749
|
-
maxDuration: "6 minutes"
|
|
1100
|
+
maxChildren: MAX_REVIEW_CHILDREN,
|
|
1101
|
+
maxConcurrency: 4,
|
|
1102
|
+
maxTurns: 6,
|
|
1103
|
+
maxToolCalls: 1,
|
|
1104
|
+
maxDuration: "6 minutes",
|
|
1105
|
+
maxResultBytes: 256 * 1024
|
|
750
1106
|
});
|
|
751
|
-
const delegationDescription = "Delegate the review of one planned unit to a bounded file-reviewer child and return its line-anchored findings. Call it exactly once per unit from list_review_units; never retry a failed unit.";
|
|
752
|
-
/**
|
|
753
|
-
* Total mapping from every expected child Run failure to the declared unit
|
|
754
|
-
* failure (SUB-028): the tag plus a bounded message, nothing else crosses.
|
|
755
|
-
*/
|
|
756
1107
|
const mapFileReviewChildFailure = (failure) => FileReviewUnitFailed.make({
|
|
757
1108
|
childErrorTag: failure._tag,
|
|
758
1109
|
message: (failure.message ?? "").slice(0, 400)
|
|
759
1110
|
});
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1140
|
+
const evidence = [];
|
|
1141
|
+
for (const shard of unit.evidenceShards) {
|
|
1142
|
+
const file = byPath.get(shard.path);
|
|
1143
|
+
if (file === void 0) return yield* rejectWork(request.workId, `planned evidence path is unavailable: ${shard.path}`);
|
|
1144
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
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}`);
|
|
1147
|
+
evidence.push(FileReviewEvidence.make({
|
|
1148
|
+
shardId: shard.shardId,
|
|
1149
|
+
path: shard.path,
|
|
1150
|
+
status: file.status,
|
|
1151
|
+
reviewMode: chunk.reviewMode,
|
|
1152
|
+
ordinal: shard.ordinal,
|
|
1153
|
+
total: shard.total,
|
|
1154
|
+
annotatedPatch: chunk.annotatedPatch
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
1157
|
+
return FileReviewBrief.make({
|
|
1158
|
+
...request,
|
|
1159
|
+
evidence
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
const candidateOrdinal = (index) => String(index + 1).padStart(3, "0");
|
|
1163
|
+
const projectReviewResult = (report, context, request) => {
|
|
1164
|
+
if (context.budgetExhausted) return Effect.fail(rejectWork(report.workId, "review work exhausted its budget before exact settlement"));
|
|
1165
|
+
if (report.phase !== request.phase || report.workId !== request.workId || report.unitId !== request.unitId) return Effect.fail(rejectWork(request.workId, "review output identity does not match the scheduled request"));
|
|
1166
|
+
if (report.phase === "verification") {
|
|
1167
|
+
if (report.findings.length > 0 || report.concerns.length > 0 || report.fileSummaries.length > 0) return Effect.fail(rejectWork(report.workId, "verification output contained discovery-only fields"));
|
|
1168
|
+
const expectedIds = new Set(request.candidates.map((candidate) => candidate.candidateId));
|
|
1169
|
+
const assessedIds = /* @__PURE__ */ new Set();
|
|
1170
|
+
for (const assessment of report.assessments) {
|
|
1171
|
+
if (!expectedIds.has(assessment.candidateId) || assessedIds.has(assessment.candidateId)) return Effect.fail(rejectWork(report.workId, "verification output did not assess the exact candidate set"));
|
|
1172
|
+
assessedIds.add(assessment.candidateId);
|
|
1173
|
+
}
|
|
1174
|
+
if (assessedIds.size !== expectedIds.size) return Effect.fail(rejectWork(report.workId, "verification output did not assess the exact candidate set"));
|
|
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
|
+
}));
|
|
1183
|
+
}
|
|
1184
|
+
if (report.assessments.length > 0) return Effect.fail(rejectWork(report.workId, "discovery output contained verification-only assessments"));
|
|
1185
|
+
const allowed = new Set(request.paths);
|
|
1186
|
+
if (report.findings.some((finding) => !allowed.has(finding.path)) || report.concerns.some((candidate) => candidate.evidencePaths.some((path) => !allowed.has(path))) || report.fileSummaries.some((entry) => !allowed.has(entry.path))) return Effect.fail(rejectWork(report.workId, "discovery output referenced evidence outside the scheduled unit"));
|
|
1187
|
+
return Effect.gen(function* () {
|
|
1188
|
+
const source = yield* PullRequestSource;
|
|
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}`);
|
|
1198
|
+
}
|
|
1199
|
+
const findingCandidates = report.findings.map((finding, index) => FindingCandidate.make({
|
|
1200
|
+
candidateId: `${request.workId}:finding:${candidateOrdinal(index)}`,
|
|
1201
|
+
workId: request.workId,
|
|
1202
|
+
unitId: request.unitId,
|
|
1203
|
+
finding,
|
|
1204
|
+
evidencePaths: [finding.path]
|
|
1205
|
+
}));
|
|
1206
|
+
const concernCandidates = report.concerns.map((candidate, index) => ConcernCandidate.make({
|
|
1207
|
+
candidateId: `${request.workId}:concern:${candidateOrdinal(index)}`,
|
|
1208
|
+
workId: request.workId,
|
|
1209
|
+
unitId: request.unitId,
|
|
1210
|
+
concern: candidate.concern,
|
|
1211
|
+
evidencePaths: candidate.evidencePaths
|
|
1212
|
+
}));
|
|
1213
|
+
return FileReviewUnitResult.make({
|
|
1214
|
+
phase: report.phase,
|
|
1215
|
+
workId: report.workId,
|
|
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") }) {};
|
|
763
1236
|
const ListReviewUnits = Tool.make("list_review_units", {
|
|
764
|
-
description: "List
|
|
1237
|
+
description: "List deterministic bounded review units, explicit risk categories, every required discovery pass, and paths the pipeline cannot cover.",
|
|
765
1238
|
parameters: ListReviewUnitsQuery,
|
|
766
1239
|
success: ReviewUnitPlan,
|
|
767
1240
|
failure: PullRequestSourceFailure,
|
|
@@ -773,80 +1246,44 @@ const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({ list_re
|
|
|
773
1246
|
const source = yield* PullRequestSource;
|
|
774
1247
|
return planReviewUnits(yield* source.changedFiles, { totalChangedFiles: (yield* source.metadata).totalChangedFiles });
|
|
775
1248
|
}) });
|
|
776
|
-
/**
|
|
777
|
-
* Build the coordinator's instructions. The same consumer guidance the
|
|
778
|
-
* children receive is injected between the mission framing and the procedure
|
|
779
|
-
* so the merged summary and verdict are shaped by the same review profile,
|
|
780
|
-
* and the configured findings bound reaches the merge step instead of only
|
|
781
|
-
* the host-side trim.
|
|
782
|
-
*/
|
|
783
1249
|
const makeFanOutReviewInstructions = (options = {}) => (mission) => {
|
|
784
1250
|
const maxFindings = clampMaxFindings(options.maxFindings);
|
|
785
1251
|
return [
|
|
786
|
-
`You coordinate the review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}
|
|
787
|
-
mission.body.length > 0 ? `Author description:\n${mission.body}` : "
|
|
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.",
|
|
788
1254
|
...staticGuidanceLines(options.guidance),
|
|
789
|
-
"
|
|
790
|
-
"
|
|
791
|
-
"
|
|
792
|
-
"
|
|
793
|
-
`
|
|
794
|
-
|
|
795
|
-
"6. Merge the units' fileSummaries into one walkthrough: copy each entry verbatim, one entry per file, dropping duplicate paths.",
|
|
796
|
-
"7. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {\"summary\": <string, 1-3 paragraphs of overall assessment, including every unreviewed unit or file>, \"verdict\": <\"approve\" | \"comment\" | \"request-changes\">, \"findings\": [{\"path\": <string>, \"startLine\": <integer>, \"endLine\": <integer>, \"severity\": <\"blocking\" | \"important\" | \"nit\">, \"category\": <string, OPTIONAL>, \"title\": <string, <= 120 chars>, \"body\": <string>, \"suggestion\": <string, OPTIONAL>}], \"concerns\": <array, OPTIONAL: [{\"severity\": <\"blocking\" | \"important\" | \"nit\">, \"title\": <string, <= 120 chars>, \"body\": <string>}], the merged unit concerns>, \"walkthrough\": <array, OPTIONAL: [{\"path\": <string>, \"summary\": <string>}], the merged fileSummaries>}. Copy findings (including \"category\" and \"suggestion\" when present), concerns, and walkthrough entries verbatim from the delegation results; never invent or edit anchors.",
|
|
797
|
-
"Use verdict \"request-changes\" only when at least one finding or concern is \"blocking\". An empty findings array with verdict \"approve\" is a valid review when every unit succeeded and found nothing."
|
|
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."
|
|
798
1261
|
].join("\n");
|
|
799
1262
|
};
|
|
800
1263
|
const fanOutReviewInstructions = makeFanOutReviewInstructions();
|
|
801
|
-
/** The default fan-out coordinator execution bounds. */
|
|
802
1264
|
const defaultFanOutPolicy = AgentPolicy.make({
|
|
803
|
-
maxTurns:
|
|
804
|
-
maxToolCalls:
|
|
805
|
-
maxDuration: "
|
|
806
|
-
toolConcurrency:
|
|
1265
|
+
maxTurns: 7,
|
|
1266
|
+
maxToolCalls: 25,
|
|
1267
|
+
maxDuration: "20 minutes",
|
|
1268
|
+
toolConcurrency: 4,
|
|
807
1269
|
repeatedFailureLimit: 3,
|
|
808
|
-
tokenBudget:
|
|
1270
|
+
tokenBudget: 4e5,
|
|
809
1271
|
contextTokenLimit: 15e4,
|
|
810
1272
|
onExhaustion: "final-answer"
|
|
811
1273
|
});
|
|
812
|
-
const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-
|
|
1274
|
+
const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-review-worker", {
|
|
813
1275
|
input: FileReviewBrief,
|
|
814
1276
|
output: FileReviewReport,
|
|
815
1277
|
instructions: makeFileReviewerInstructions(options),
|
|
816
1278
|
toolkit: FileReviewToolkit,
|
|
817
1279
|
policy: defaultFileReviewerPolicy,
|
|
818
|
-
description: "
|
|
1280
|
+
description: "Perform one bounded discovery or independent candidate-verification pass over host-supplied pull-request evidence.",
|
|
819
1281
|
metadata: {
|
|
820
1282
|
deploymentClass: "E",
|
|
821
|
-
surface: "read-only"
|
|
1283
|
+
surface: "read-only",
|
|
1284
|
+
stage: "discovery-verification"
|
|
822
1285
|
}
|
|
823
1286
|
});
|
|
824
|
-
const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_review", {
|
|
825
|
-
description: delegationDescription,
|
|
826
|
-
target: child,
|
|
827
|
-
parameters: FileReviewRequest,
|
|
828
|
-
success: FileReviewUnitResult,
|
|
829
|
-
failure: FileReviewUnitFailed,
|
|
830
|
-
failureMode: "return",
|
|
831
|
-
prepareInput: (request) => Effect.succeed(FileReviewBrief.make({
|
|
832
|
-
unitId: request.unitId,
|
|
833
|
-
paths: request.paths,
|
|
834
|
-
focus: "defects-first: correctness, security, concurrency, resources, error handling"
|
|
835
|
-
})),
|
|
836
|
-
projectResult: (report) => Effect.succeed(FileReviewUnitResult.make({
|
|
837
|
-
unitId: report.unitId,
|
|
838
|
-
findings: report.findings,
|
|
839
|
-
...report.concerns !== void 0 ? { concerns: report.concerns } : {},
|
|
840
|
-
...report.fileSummaries !== void 0 ? { fileSummaries: report.fileSummaries } : {}
|
|
841
|
-
})),
|
|
842
|
-
policy: fileReviewPolicy
|
|
843
|
-
});
|
|
844
|
-
/**
|
|
845
|
-
* The coordinator-facing delegation Tool: the delegation's own first-party
|
|
846
|
-
* contained Tool plus the read-only execution class (the delegated child's
|
|
847
|
-
* whole tool surface is read-only). Effect AI resolves handlers by Tool name,
|
|
848
|
-
* so `SubagentRuntime.layer`'s handler serves this annotated copy unchanged.
|
|
849
|
-
*/
|
|
850
1287
|
const delegationToolFor = (delegation) => delegation.tool.annotate(ToolExecutionClass, "readonly");
|
|
851
1288
|
const makeFanOutReviewerDefinition = (options, delegation) => Agent.define("pr-fanout-reviewer", {
|
|
852
1289
|
input: ReviewMission,
|
|
@@ -854,14 +1291,14 @@ const makeFanOutReviewerDefinition = (options, delegation) => Agent.define("pr-f
|
|
|
854
1291
|
instructions: makeFanOutReviewInstructions(options),
|
|
855
1292
|
toolkit: Toolkit.make(ListReviewUnits, delegationToolFor(delegation)),
|
|
856
1293
|
policy: defaultFanOutPolicy,
|
|
857
|
-
description: "Coordinate
|
|
1294
|
+
description: "Coordinate deterministic general/specialist discovery and independent candidate verification over bounded review units.",
|
|
858
1295
|
metadata: {
|
|
859
1296
|
deploymentClass: "E",
|
|
860
1297
|
surface: "read-only",
|
|
861
|
-
delegation: "S1-attached"
|
|
1298
|
+
delegation: "S1-attached",
|
|
1299
|
+
assurance: "multi-pass"
|
|
862
1300
|
}
|
|
863
1301
|
});
|
|
864
|
-
/** Build one coherent fan-out suite: child, coordinator, and delegation. */
|
|
865
1302
|
const makeFanOutReviewSuite = (options = {}) => {
|
|
866
1303
|
const child = makeFileReviewerDefinition({ guidance: options.guidance });
|
|
867
1304
|
const delegation = makeFileReviewDelegation(child);
|
|
@@ -872,25 +1309,13 @@ const makeFanOutReviewSuite = (options = {}) => {
|
|
|
872
1309
|
};
|
|
873
1310
|
};
|
|
874
1311
|
const defaultSuite = makeFanOutReviewSuite();
|
|
875
|
-
/** The default child Agent Definition. */
|
|
876
1312
|
const FileReviewer = defaultSuite.child;
|
|
877
|
-
/** The default coordinator Agent Definition. */
|
|
878
1313
|
const FanOutReviewer = defaultSuite.parent;
|
|
879
|
-
/** The default delegation over the default child. */
|
|
880
1314
|
const fileReviewDelegation = defaultSuite.delegation;
|
|
881
|
-
/** The default coordinator-facing delegation Tool (first-party contained mode). */
|
|
882
1315
|
const DelegateFileReview = delegationToolFor(fileReviewDelegation);
|
|
883
|
-
/** The default coordinator Toolkit. */
|
|
884
1316
|
const FanOutReviewToolkit = FanOutReviewer.toolkit;
|
|
885
|
-
/**
|
|
886
|
-
* The contained failure family the delegation can surface as result data
|
|
887
|
-
* (SUB-033), derived from the delegation itself so the coverage decoder can
|
|
888
|
-
* never diverge from what the runtime actually contains.
|
|
889
|
-
*/
|
|
890
1317
|
const FileReviewDelegationFailure = fileReviewDelegation.containedFailure;
|
|
891
|
-
/** Runtime wiring: one delegation plus one explicit child Binding. */
|
|
892
1318
|
const fanOutHandlersLayerFor = (delegation) => (childBinding) => SubagentRuntime.layer(delegation, childBinding, { mapChildFailure: mapFileReviewChildFailure });
|
|
893
|
-
/** Runtime wiring over the default delegation, mirroring the leaf example. */
|
|
894
1319
|
const fanOutHandlersLayer = fanOutHandlersLayerFor(fileReviewDelegation);
|
|
895
1320
|
//#endregion
|
|
896
1321
|
//#region src/internal/fingerprint.ts
|
|
@@ -957,10 +1382,12 @@ var StoredReviewConcern = class extends Schema.Class("@effect-agent/pr-review/St
|
|
|
957
1382
|
body: StoredText
|
|
958
1383
|
}) {};
|
|
959
1384
|
/**
|
|
960
|
-
* Versioned state embedded
|
|
961
|
-
* head plus
|
|
962
|
-
*
|
|
963
|
-
*
|
|
1385
|
+
* Versioned state embedded only after complete input assignment and settled
|
|
1386
|
+
* configured review assurance. The head plus full-scope fingerprint forms an
|
|
1387
|
+
* incremental baseline; an absent unresolved item never means the path is
|
|
1388
|
+
* defect-free. The `acceptedScopeFingerprint` name is retained for wire
|
|
1389
|
+
* compatibility. Storing hundreds of path strings separately would not fit
|
|
1390
|
+
* GitHub's bounded review body in the worst case.
|
|
964
1391
|
*/
|
|
965
1392
|
var ReviewState = class extends Schema.Class("@effect-agent/pr-review/ReviewState")({
|
|
966
1393
|
version: Schema.Literal(1),
|
|
@@ -1170,7 +1597,7 @@ const selectReviewRange = (input) => {
|
|
|
1170
1597
|
const selectedFiles = [...selectedByPath.values()].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1171
1598
|
return {
|
|
1172
1599
|
mode: "incremental",
|
|
1173
|
-
reason: `changes since
|
|
1600
|
+
reason: `changes since settled review head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}`,
|
|
1174
1601
|
files: selectedFiles,
|
|
1175
1602
|
affectedPaths: [...affectedPaths].sort(),
|
|
1176
1603
|
totalFiles: selectedFiles.length,
|
|
@@ -1842,6 +2269,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
|
|
|
1842
2269
|
return Option.isSome(latest) && latest.value === current;
|
|
1843
2270
|
});
|
|
1844
2271
|
//#endregion
|
|
1845
|
-
export {
|
|
2272
|
+
export { DiscoveredConcern as $, ChangedFile as $n, ReviewUnit as $t, ReviewStateAuthenticator as A, ReviewMission as An, fanOutHandlersLayer as At, selectedPullRequestSourceLayer as B, makeReviewInstructions as Bn, reviewCandidateSubjectKey as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, MAX_WALKTHROUGH_SUMMARY_CHARS as Cn, MAX_UNIT_CANDIDATES as Ct, ReviewScopeMode as D, ReadFileDiff as Dn, ReviewWorkPhase as Dt, ReviewMode as E, ReadFile as En, ReviewWorkPerspective as Et, buildProfileMission as F, clampMaxFindings as Fn, fileReviewerInstructions as Ft, webCryptoReviewStateAuthenticatorLayer as G, MAX_CHANGED_FILES as Gn, MAX_UNIT_EVIDENCE_SHARDS as Gt, toStoredFinding as H, readFileHandler as Hn, MAX_MERGED_FINDINGS as Ht, computeProfileFingerprint as I, defaultReviewPolicy as In, makeFanOutReviewInstructions as It, extractFingerprint as J, PullRequestSource as Jn, ReviewDiscoveryPerspective as Jt, FINGERPRINT_MARKER_LENGTH as K, MAX_FILE_CHARS as Kn, MAX_UNIT_FILES as Kt, fromStoredConcern as L, fileDiffView as Ln, makeFanOutReviewSuite as Lt, ReviewStateMarkerTooLarge as M, ReviewToolkitLayer as Mn, fanOutReviewInstructions as Mt, StoredReviewConcern as N, ReviewVerdict as Nn, fileReviewDelegation as Nt, ReviewState as O, ReviewConcern as On, defaultFanOutPolicy as Ot, StoredReviewFinding as P, WalkthroughEntry as Pn, fileReviewPolicy as Pt, DelegateFileReview as Q, anchorViolation as Qn, ReviewRiskCategory as Qt, fromStoredFinding as R, fileReviewEvidenceChunks as Rn, makeFileReviewerInstructions as Rt, GitCommitSha as S, MAX_WALKTHROUGH_ENTRIES as Sn, MAX_REVIEW_CHILDREN as St, ReviewHeadComparison as T, REVIEW_TOOL_RESULT_MAX_BYTES as Tn, ReviewCandidateId as Tt, unavailableReviewStateAuthenticatorLayer as U, resolveGuidance as Un, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as Ut, toStoredConcern as V, readFileDiffHandler as Vn, MAX_FILE_EVIDENCE_CHARS as Vt, validateReviewState as W, reviewInstructions as Wn, MAX_REVIEW_UNITS as Wt, CandidateAssessment as X, ReviewInputViolation as Xn, ReviewEvidenceShardId as Xt, renderFingerprintMarker as Y, PullRequestSourceFailure as Yn, ReviewEvidenceShard as Yt, ConcernCandidate as Z, normalizeRepoRelativePath as Zn, ReviewPassId as Zt, ReviewRetirementHost as _, ListChangedFiles as _n, ListReviewUnits as _t, PriorReviews as a, findingAnchorInUnitEvidence as an, hasReviewableContent as ar, FileReviewDelegationFailure as at, hasReviewMetadataMarker as b, MAX_FINDINGS as bn, MAX_CHILD_FINDINGS as bt, fingerprintUnchanged as c, ChangedFileSummary as cn, renderReviewContent as cr, FileReviewReport as ct, gitHubReviewPublisherLayer as d, FileDiffQuery as dn, FileReviewToolkitLayer as dt, ReviewUnitId as en, ChangedFileStatus as er, FanOutCoordinatorToolkit as et, gitHubReviewRetirementHostLayer as f, FileDiffView as fn, FileReviewUnitFailed as ft, ReviewRetirementFailure as g, FindingSeverity as gn, FindingCandidate as gt, RetirableReviewComment as h, FindingCategory as hn, FileReviewer as ht, PriorReviewLookupFailure as i, classifyReviewRisks as in, commentableLines as ir, FileReviewBrief as it, ReviewStateMarker as j, ReviewToolkit as jn, fanOutHandlersLayerFor as jt, ReviewStateAuthenticationFailure as k, ReviewFinding as kn, defaultFileReviewerPolicy as kt, gitHubPriorReviewsLayer as l, ChangedFilesView as ln, FileReviewRequest as lt, RetirableReview as m, FileSliceQuery as mn, FileReviewWorkRejected as mt, GitHubApiFailure as n, UNIT_CHANGED_LINE_BUDGET as nn, MAX_REVIEW_CONTENT_CHARS as nr, FanOutReviewToolkit as nt, PublishedReview as o, planReviewUnits as on, isReviewableFile as or, FileReviewEvidence as ot, parseGitHubSubmittedAt as p, FileSlice as pn, FileReviewUnitResult as pt, computeChangesetFingerprint as q, PullRequestMetadata as qn, ReviewDiscoveryPass as qt, GitHubReviewTarget as r, UNIT_EVIDENCE_CHAR_BUDGET as rn, annotatePatch as rr, FanOutReviewer as rt, ReviewPublisher as s, rankAndDedupeFindings as sn, parsePatch as sr, FileReviewFailure as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, ReviewUnitPlan as tn, ChangedPath as tr, FanOutCoordinatorToolkitLayer as tt, gitHubPullRequestSourceLayer as u, CodeReview as un, FileReviewToolkit as ut, ReviewRetirementReport as v, ListChangedFilesQuery as vn, ListReviewUnitsQuery as vt, ReviewExecutionContext as w, PullRequestReviewer as wn, ReviewCandidate as wt, retireStaleReviews as x, MAX_PATCH_CHARS as xn, MAX_FILE_REVIEW_TOOL_CALLS as xt, decideReviewRetirement as y, MAX_CONCERNS as yn, MAX_CHILD_CONCERNS as yt, selectReviewRange as z, listChangedFilesHandler as zn, mapFileReviewChildFailure as zt };
|
|
1846
2273
|
|
|
1847
|
-
//# sourceMappingURL=github-
|
|
2274
|
+
//# sourceMappingURL=github-DSqZp3Ce.mjs.map
|