@effect-agent/pr-review 0.1.0-beta.20 → 0.1.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -26
- package/dist/action.d.mts +12 -6
- package/dist/action.mjs +17 -27
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-CQFA-o0v.d.mts → fan-out-n-00ppWr.d.mts} +308 -223
- package/dist/{github-DnenG3be.mjs → github-BgtP7Rdv.mjs} +652 -196
- package/dist/github-BgtP7Rdv.mjs.map +1 -0
- package/dist/index.d.mts +27 -54
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BE83_Tfo.mjs → providers-BguZK4B_.mjs} +408 -134
- package/dist/providers-BguZK4B_.mjs.map +1 -0
- package/dist/testing.d.mts +10 -47
- package/dist/testing.mjs +29 -55
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -39
- package/src/index.ts +1 -0
- package/src/internal/anchors.ts +20 -0
- package/src/internal/coverage.ts +628 -134
- package/src/internal/factory.ts +12 -9
- package/src/internal/fan-out-scripted.ts +61 -94
- package/src/internal/fan-out.ts +560 -255
- package/src/internal/profiles.ts +12 -0
- package/src/internal/progress.ts +1 -1
- package/src/internal/render.ts +51 -25
- package/src/internal/review-agent.ts +74 -25
- package/src/internal/review-state.ts +7 -5
- package/src/internal/review-units.ts +326 -32
- package/src/internal/run.ts +48 -41
- package/dist/github-DnenG3be.mjs.map +0 -1
- package/dist/providers-BE83_Tfo.mjs.map +0 -1
|
@@ -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
|
|
@@ -434,7 +483,7 @@ var ReviewFinding = class extends Schema.Class("@effect-agent/pr-review/ReviewFi
|
|
|
434
483
|
title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
|
|
435
484
|
body: Schema.NonEmptyString.check(Schema.isMaxLength(2e3)),
|
|
436
485
|
/** Replacement for exactly lines startLine..endLine; omit when unsure. */
|
|
437
|
-
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)))
|
|
438
487
|
}) {};
|
|
439
488
|
const ReviewVerdict = Schema.Literals([
|
|
440
489
|
"approve",
|
|
@@ -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;
|
|
571
796
|
};
|
|
572
|
-
const
|
|
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;
|
|
821
|
+
};
|
|
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,340 @@ 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
|
+
/**
|
|
971
|
+
* Exact suggestion settlement: required when the candidate finding carries
|
|
972
|
+
* a suggestion, forbidden otherwise. Untrusted child output cannot publish
|
|
973
|
+
* a GitHub replacement block by prompt compliance alone — the host keeps a
|
|
974
|
+
* confirmed finding's suggestion only on an exact "committable" settlement.
|
|
975
|
+
*/
|
|
976
|
+
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." })),
|
|
977
|
+
rationale: Schema.NonEmptyString.check(Schema.isMaxLength(600))
|
|
978
|
+
}) {};
|
|
659
979
|
/**
|
|
660
|
-
*
|
|
661
|
-
*
|
|
980
|
+
* Exact suggestion settlement shape: a carried suggestion must be settled and
|
|
981
|
+
* nothing else may be. Enforced identically by the live delegation projection
|
|
982
|
+
* and the independent host coverage fold.
|
|
662
983
|
*/
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
984
|
+
const assessmentSettlesSuggestionExactly = (assessment, candidate) => candidate._tag === "FindingCandidate" && candidate.finding.suggestion !== void 0 ? assessment.suggestion !== void 0 : assessment.suggestion === void 0;
|
|
985
|
+
/**
|
|
986
|
+
* Fail-closed publication of a confirmed finding: only an exact "committable"
|
|
987
|
+
* settlement keeps the suggestion; anything else publishes the finding with
|
|
988
|
+
* the suggestion stripped so unverified text can never become a one-click
|
|
989
|
+
* GitHub replacement block.
|
|
990
|
+
*/
|
|
991
|
+
const confirmedFindingForPublication = (assessment, candidate) => {
|
|
992
|
+
if (candidate.finding.suggestion === void 0 || assessment.suggestion === "committable") return candidate.finding;
|
|
993
|
+
const { suggestion: _stripped, ...finding } = candidate.finding;
|
|
994
|
+
return ReviewFinding.make(finding);
|
|
995
|
+
};
|
|
996
|
+
/**
|
|
997
|
+
* Concern candidates need explicit paths internally to bind the claim to
|
|
998
|
+
* scheduled evidence. The verifier receives the complete bounded unit so it
|
|
999
|
+
* can use neighboring evidence to falsify the claim. The public ReviewConcern
|
|
1000
|
+
* remains path-free after the host confirms and projects it.
|
|
1001
|
+
*/
|
|
1002
|
+
var DiscoveredConcern = class extends Schema.Class("@effect-agent/pr-review/DiscoveredConcern")({
|
|
1003
|
+
concern: ReviewConcern,
|
|
1004
|
+
evidencePaths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(3))
|
|
1005
|
+
}) {};
|
|
669
1006
|
const UnitPaths = Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
|
|
670
|
-
|
|
1007
|
+
const RiskCategories = Schema.Array(ReviewRiskCategory).check(Schema.isMaxLength(6));
|
|
1008
|
+
const Candidates = Schema.Array(ReviewCandidate).check(Schema.isMaxLength(18));
|
|
1009
|
+
const EvidenceShardIds = Schema.Array(ReviewEvidenceShardId).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12));
|
|
1010
|
+
/** Strict-object coordinator request for either discovery or verification. */
|
|
1011
|
+
var FileReviewRequest = class extends Schema.Class("@effect-agent/pr-review/FileReviewRequest")({
|
|
1012
|
+
phase: ReviewWorkPhase,
|
|
1013
|
+
workId: ReviewPassId,
|
|
1014
|
+
unitId: ReviewUnitId,
|
|
1015
|
+
paths: UnitPaths,
|
|
1016
|
+
evidenceShardIds: EvidenceShardIds,
|
|
1017
|
+
perspective: ReviewWorkPerspective,
|
|
1018
|
+
riskCategories: RiskCategories,
|
|
1019
|
+
/** Empty for discovery; the exact discovered set for unit verification. */
|
|
1020
|
+
candidates: Candidates
|
|
1021
|
+
}) {};
|
|
1022
|
+
/** One complete host-selected evidence shard supplied to a review child. */
|
|
1023
|
+
var FileReviewEvidence = class extends Schema.Class("@effect-agent/pr-review/FileReviewEvidence")({
|
|
1024
|
+
shardId: ReviewEvidenceShardId,
|
|
1025
|
+
path: ChangedPath,
|
|
1026
|
+
status: ChangedFileStatus,
|
|
1027
|
+
reviewMode: Schema.Literals([
|
|
1028
|
+
"diff",
|
|
1029
|
+
"content",
|
|
1030
|
+
"unavailable"
|
|
1031
|
+
]),
|
|
1032
|
+
ordinal: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
1033
|
+
total: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),
|
|
1034
|
+
annotatedPatch: Schema.String.check(Schema.isMaxLength(MAX_PATCH_CHARS))
|
|
1035
|
+
}) {};
|
|
1036
|
+
/** Host-prepared child input with complete bounded diff/content evidence. */
|
|
671
1037
|
var FileReviewBrief = class extends Schema.Class("@effect-agent/pr-review/FileReviewBrief")({
|
|
1038
|
+
phase: ReviewWorkPhase,
|
|
1039
|
+
workId: ReviewPassId,
|
|
672
1040
|
unitId: ReviewUnitId,
|
|
673
1041
|
paths: UnitPaths,
|
|
674
|
-
|
|
1042
|
+
evidenceShardIds: EvidenceShardIds,
|
|
1043
|
+
perspective: ReviewWorkPerspective,
|
|
1044
|
+
riskCategories: RiskCategories,
|
|
1045
|
+
candidates: Candidates,
|
|
1046
|
+
evidence: Schema.Array(FileReviewEvidence).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12))
|
|
675
1047
|
}) {};
|
|
676
|
-
/**
|
|
1048
|
+
/** Child output; phase-inapplicable collections must be empty. */
|
|
677
1049
|
var FileReviewReport = class extends Schema.Class("@effect-agent/pr-review/FileReviewReport")({
|
|
1050
|
+
phase: ReviewWorkPhase,
|
|
1051
|
+
workId: ReviewPassId,
|
|
678
1052
|
unitId: ReviewUnitId,
|
|
679
|
-
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
fileSummaries: Schema.optionalKey(Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)))
|
|
1053
|
+
findings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(6)),
|
|
1054
|
+
concerns: Schema.Array(DiscoveredConcern).check(Schema.isMaxLength(3)),
|
|
1055
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)),
|
|
1056
|
+
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(18))
|
|
684
1057
|
}) {};
|
|
1058
|
+
/** Bounded coordinator-visible result with host-assigned candidate IDs. */
|
|
1059
|
+
var FileReviewUnitResult = class extends Schema.Class("@effect-agent/pr-review/FileReviewUnitResult")({
|
|
1060
|
+
phase: ReviewWorkPhase,
|
|
1061
|
+
workId: ReviewPassId,
|
|
1062
|
+
unitId: ReviewUnitId,
|
|
1063
|
+
candidates: Candidates,
|
|
1064
|
+
fileSummaries: Schema.Array(WalkthroughEntry).check(Schema.isMaxLength(12)),
|
|
1065
|
+
assessments: Schema.Array(CandidateAssessment).check(Schema.isMaxLength(18))
|
|
1066
|
+
}) {};
|
|
1067
|
+
var FileReviewUnitFailed = class extends Schema.TaggedError()("FileReviewUnitFailed", {
|
|
1068
|
+
childErrorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
|
|
1069
|
+
message: Schema.String.check(Schema.isMaxLength(400))
|
|
1070
|
+
}) {};
|
|
1071
|
+
var FileReviewWorkRejected = class extends Schema.TaggedError()("FileReviewWorkRejected", {
|
|
1072
|
+
workId: ReviewPassId,
|
|
1073
|
+
reason: Schema.NonEmptyString.check(Schema.isMaxLength(600))
|
|
1074
|
+
}) {};
|
|
1075
|
+
const FileReviewFailure = Schema.Union([FileReviewUnitFailed, FileReviewWorkRejected]);
|
|
685
1076
|
const staticGuidanceLines = (guidance) => {
|
|
686
1077
|
if (guidance === void 0) return [];
|
|
687
1078
|
return (typeof guidance === "string" ? [guidance] : guidance).filter((line) => line.length > 0);
|
|
688
1079
|
};
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1080
|
+
const evidenceInstructions = [
|
|
1081
|
+
"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.",
|
|
1082
|
+
"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.",
|
|
1083
|
+
"A diff marks new-version anchors as R<number>; only those lines may anchor findings. B/H content evidence is non-anchorable."
|
|
1084
|
+
];
|
|
1085
|
+
/** Discovery and verification instructions share one child definition. */
|
|
1086
|
+
const makeFileReviewerInstructions = (options = {}) => (brief) => {
|
|
1087
|
+
const common = [
|
|
1088
|
+
`You are an attached review worker for ${brief.workId} in host-planned unit ${brief.unitId}: ${brief.paths.join(", ")}.`,
|
|
1089
|
+
...staticGuidanceLines(options.guidance),
|
|
1090
|
+
...evidenceInstructions
|
|
1091
|
+
];
|
|
1092
|
+
if (brief.phase === "verification") return [
|
|
1093
|
+
...common,
|
|
1094
|
+
"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.",
|
|
1095
|
+
"The evidence array contains the complete bounded unit, including neighboring changed code that may confirm or falsify a locally plausible claim.",
|
|
1096
|
+
"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.",
|
|
1097
|
+
"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.",
|
|
1098
|
+
"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."
|
|
1099
|
+
].join("\n");
|
|
1100
|
+
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.";
|
|
1101
|
+
return [
|
|
1102
|
+
...common,
|
|
1103
|
+
focus,
|
|
1104
|
+
"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.",
|
|
1105
|
+
"When a non-anchored concern depends on one or more unit files, list 1-3 exact evidencePaths to bind the claim to scheduled evidence.",
|
|
1106
|
+
`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.`,
|
|
1107
|
+
"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>}.",
|
|
1108
|
+
"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\"."
|
|
1109
|
+
].join("\n");
|
|
1110
|
+
};
|
|
703
1111
|
const fileReviewerInstructions = makeFileReviewerInstructions();
|
|
704
|
-
|
|
1112
|
+
const FileReviewToolkit = Toolkit.empty;
|
|
1113
|
+
/** Compatibility export: the evidence-only child has no handler requirements. */
|
|
1114
|
+
const FileReviewToolkitLayer = Layer.empty;
|
|
705
1115
|
const defaultFileReviewerPolicy = AgentPolicy.make({
|
|
706
|
-
maxTurns:
|
|
707
|
-
maxToolCalls:
|
|
1116
|
+
maxTurns: 6,
|
|
1117
|
+
maxToolCalls: 1,
|
|
708
1118
|
maxDuration: "6 minutes",
|
|
709
1119
|
toolConcurrency: 2,
|
|
710
|
-
repeatedFailureLimit:
|
|
1120
|
+
repeatedFailureLimit: 6,
|
|
711
1121
|
tokenBudget: 2e5,
|
|
712
1122
|
contextTokenLimit: 15e4,
|
|
713
1123
|
toolResultBounds: ToolResultBounds.make({ maxBytes: REVIEW_TOOL_RESULT_MAX_BYTES }),
|
|
714
1124
|
onExhaustion: "fail"
|
|
715
1125
|
});
|
|
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
1126
|
const fileReviewPolicy = SubagentPolicy.make({
|
|
745
|
-
maxChildren:
|
|
746
|
-
maxConcurrency:
|
|
747
|
-
maxTurns:
|
|
748
|
-
maxToolCalls:
|
|
749
|
-
maxDuration: "6 minutes"
|
|
1127
|
+
maxChildren: MAX_REVIEW_CHILDREN,
|
|
1128
|
+
maxConcurrency: 4,
|
|
1129
|
+
maxTurns: 6,
|
|
1130
|
+
maxToolCalls: 1,
|
|
1131
|
+
maxDuration: "6 minutes",
|
|
1132
|
+
maxResultBytes: 256 * 1024
|
|
750
1133
|
});
|
|
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
1134
|
const mapFileReviewChildFailure = (failure) => FileReviewUnitFailed.make({
|
|
757
1135
|
childErrorTag: failure._tag,
|
|
758
1136
|
message: (failure.message ?? "").slice(0, 400)
|
|
759
1137
|
});
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1138
|
+
const sameStrings = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1139
|
+
const rejectWork = (workId, reason) => FileReviewWorkRejected.make({
|
|
1140
|
+
workId,
|
|
1141
|
+
reason
|
|
1142
|
+
});
|
|
1143
|
+
/** Validate coordinator scheduling against the current deterministic plan. */
|
|
1144
|
+
const prepareReviewBrief = (request) => Effect.gen(function* () {
|
|
1145
|
+
const source = yield* PullRequestSource;
|
|
1146
|
+
const mapSourceFailure = (failure) => rejectWork(request.workId, `pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600));
|
|
1147
|
+
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
1148
|
+
const plan = planReviewUnits(files, { totalChangedFiles: (yield* source.metadata.pipe(Effect.mapError(mapSourceFailure))).totalChangedFiles });
|
|
1149
|
+
const unit = plan.units.find((candidate) => candidate.unitId === request.unitId);
|
|
1150
|
+
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");
|
|
1151
|
+
if (request.phase === "discovery") {
|
|
1152
|
+
const pass = plan.discoveryPasses.find((candidate) => candidate.passId === request.workId);
|
|
1153
|
+
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");
|
|
1154
|
+
} else {
|
|
1155
|
+
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");
|
|
1156
|
+
const candidateIds = /* @__PURE__ */ new Set();
|
|
1157
|
+
const candidateSubjects = /* @__PURE__ */ new Set();
|
|
1158
|
+
const allowed = new Set(unit.paths);
|
|
1159
|
+
for (const candidate of request.candidates) {
|
|
1160
|
+
const subjectKey = reviewCandidateSubjectKey(candidate);
|
|
1161
|
+
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");
|
|
1162
|
+
candidateIds.add(candidate.candidateId);
|
|
1163
|
+
candidateSubjects.add(subjectKey);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
1167
|
+
const evidence = [];
|
|
1168
|
+
for (const shard of unit.evidenceShards) {
|
|
1169
|
+
const file = byPath.get(shard.path);
|
|
1170
|
+
if (file === void 0) return yield* rejectWork(request.workId, `planned evidence path is unavailable: ${shard.path}`);
|
|
1171
|
+
const chunks = fileReviewEvidenceChunks(file);
|
|
1172
|
+
const chunk = chunks[shard.ordinal - 1];
|
|
1173
|
+
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}`);
|
|
1174
|
+
evidence.push(FileReviewEvidence.make({
|
|
1175
|
+
shardId: shard.shardId,
|
|
1176
|
+
path: shard.path,
|
|
1177
|
+
status: file.status,
|
|
1178
|
+
reviewMode: chunk.reviewMode,
|
|
1179
|
+
ordinal: shard.ordinal,
|
|
1180
|
+
total: shard.total,
|
|
1181
|
+
annotatedPatch: chunk.annotatedPatch
|
|
1182
|
+
}));
|
|
1183
|
+
}
|
|
1184
|
+
return FileReviewBrief.make({
|
|
1185
|
+
...request,
|
|
1186
|
+
evidence
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
const candidateOrdinal = (index) => String(index + 1).padStart(3, "0");
|
|
1190
|
+
const projectReviewResult = (report, context, request) => {
|
|
1191
|
+
if (context.budgetExhausted) return Effect.fail(rejectWork(report.workId, "review work exhausted its budget before exact settlement"));
|
|
1192
|
+
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"));
|
|
1193
|
+
if (report.phase === "verification") {
|
|
1194
|
+
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"));
|
|
1195
|
+
const expectedById = new Map(request.candidates.map((candidate) => [candidate.candidateId, candidate]));
|
|
1196
|
+
const assessedIds = /* @__PURE__ */ new Set();
|
|
1197
|
+
for (const assessment of report.assessments) {
|
|
1198
|
+
const candidate = expectedById.get(assessment.candidateId);
|
|
1199
|
+
if (candidate === void 0 || assessedIds.has(assessment.candidateId)) return Effect.fail(rejectWork(report.workId, "verification output did not assess the exact candidate set"));
|
|
1200
|
+
if (!assessmentSettlesSuggestionExactly(assessment, candidate)) return Effect.fail(rejectWork(report.workId, "verification output did not settle suggestion publication exactly"));
|
|
1201
|
+
assessedIds.add(assessment.candidateId);
|
|
1202
|
+
}
|
|
1203
|
+
if (assessedIds.size !== expectedById.size) return Effect.fail(rejectWork(report.workId, "verification output did not assess the exact candidate set"));
|
|
1204
|
+
return Effect.succeed(FileReviewUnitResult.make({
|
|
1205
|
+
phase: report.phase,
|
|
1206
|
+
workId: report.workId,
|
|
1207
|
+
unitId: report.unitId,
|
|
1208
|
+
candidates: [],
|
|
1209
|
+
fileSummaries: [],
|
|
1210
|
+
assessments: report.assessments
|
|
1211
|
+
}));
|
|
1212
|
+
}
|
|
1213
|
+
if (report.assessments.length > 0) return Effect.fail(rejectWork(report.workId, "discovery output contained verification-only assessments"));
|
|
1214
|
+
const allowed = new Set(request.paths);
|
|
1215
|
+
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"));
|
|
1216
|
+
return Effect.gen(function* () {
|
|
1217
|
+
const source = yield* PullRequestSource;
|
|
1218
|
+
const mapSourceFailure = (failure) => rejectWork(request.workId, `pull-request source ${failure.operation} failed: ${failure.reason}`.slice(0, 600));
|
|
1219
|
+
const files = yield* source.changedFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
1220
|
+
const metadata = yield* source.metadata.pipe(Effect.mapError(mapSourceFailure));
|
|
1221
|
+
const anchorFiles = yield* source.anchorFiles.pipe(Effect.mapError(mapSourceFailure));
|
|
1222
|
+
const unit = planReviewUnits(files, { totalChangedFiles: metadata.totalChangedFiles }).units.find((candidate) => candidate.unitId === request.unitId);
|
|
1223
|
+
if (unit === void 0) return yield* rejectWork(request.workId, "scheduled review unit is no longer available");
|
|
1224
|
+
for (const finding of report.findings) {
|
|
1225
|
+
const violation = anchorViolation(finding, anchorFiles);
|
|
1226
|
+
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}`);
|
|
1227
|
+
}
|
|
1228
|
+
const findingCandidates = report.findings.map((finding, index) => FindingCandidate.make({
|
|
1229
|
+
candidateId: `${request.workId}:finding:${candidateOrdinal(index)}`,
|
|
1230
|
+
workId: request.workId,
|
|
1231
|
+
unitId: request.unitId,
|
|
1232
|
+
finding,
|
|
1233
|
+
evidencePaths: [finding.path]
|
|
1234
|
+
}));
|
|
1235
|
+
const concernCandidates = report.concerns.map((candidate, index) => ConcernCandidate.make({
|
|
1236
|
+
candidateId: `${request.workId}:concern:${candidateOrdinal(index)}`,
|
|
1237
|
+
workId: request.workId,
|
|
1238
|
+
unitId: request.unitId,
|
|
1239
|
+
concern: candidate.concern,
|
|
1240
|
+
evidencePaths: candidate.evidencePaths
|
|
1241
|
+
}));
|
|
1242
|
+
return FileReviewUnitResult.make({
|
|
1243
|
+
phase: report.phase,
|
|
1244
|
+
workId: report.workId,
|
|
1245
|
+
unitId: report.unitId,
|
|
1246
|
+
candidates: [...findingCandidates, ...concernCandidates],
|
|
1247
|
+
fileSummaries: report.fileSummaries,
|
|
1248
|
+
assessments: []
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
};
|
|
1252
|
+
const delegationDescription = "Run exactly one host-planned discovery or candidate-verification child. Copy every plan field and candidate verbatim; never retry failed work.";
|
|
1253
|
+
const makeFileReviewDelegation = (child) => Subagent.define("delegate_file_review", {
|
|
1254
|
+
description: delegationDescription,
|
|
1255
|
+
target: child,
|
|
1256
|
+
parameters: FileReviewRequest,
|
|
1257
|
+
success: FileReviewUnitResult,
|
|
1258
|
+
failure: FileReviewFailure,
|
|
1259
|
+
failureMode: "return",
|
|
1260
|
+
prepareInput: prepareReviewBrief,
|
|
1261
|
+
projectResult: projectReviewResult,
|
|
1262
|
+
policy: fileReviewPolicy
|
|
1263
|
+
});
|
|
1264
|
+
var ListReviewUnitsQuery = class extends Schema.Class("@effect-agent/pr-review/ListReviewUnitsQuery")({ scope: Schema.Literal("all") }) {};
|
|
763
1265
|
const ListReviewUnits = Tool.make("list_review_units", {
|
|
764
|
-
description: "List
|
|
1266
|
+
description: "List deterministic bounded review units, explicit risk categories, every required discovery pass, and paths the pipeline cannot cover.",
|
|
765
1267
|
parameters: ListReviewUnitsQuery,
|
|
766
1268
|
success: ReviewUnitPlan,
|
|
767
1269
|
failure: PullRequestSourceFailure,
|
|
@@ -773,80 +1275,44 @@ const FanOutCoordinatorToolkitLayer = FanOutCoordinatorToolkit.toLayer({ list_re
|
|
|
773
1275
|
const source = yield* PullRequestSource;
|
|
774
1276
|
return planReviewUnits(yield* source.changedFiles, { totalChangedFiles: (yield* source.metadata).totalChangedFiles });
|
|
775
1277
|
}) });
|
|
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
1278
|
const makeFanOutReviewInstructions = (options = {}) => (mission) => {
|
|
784
1279
|
const maxFindings = clampMaxFindings(options.maxFindings);
|
|
785
1280
|
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}` : "
|
|
1281
|
+
`You coordinate the bounded multi-pass review of pull request #${mission.number} ("${mission.title}") in ${mission.repository}.`,
|
|
1282
|
+
mission.body.length > 0 ? `Author description:\n${mission.body}` : "No author description.",
|
|
788
1283
|
...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."
|
|
1284
|
+
"1. Call list_review_units exactly once.",
|
|
1285
|
+
"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.",
|
|
1286
|
+
"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.",
|
|
1287
|
+
"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.",
|
|
1288
|
+
`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}.`,
|
|
1289
|
+
"No configured pipeline can prove absence of defects. Describe settled work, never an exhaustive or defect-free review."
|
|
798
1290
|
].join("\n");
|
|
799
1291
|
};
|
|
800
1292
|
const fanOutReviewInstructions = makeFanOutReviewInstructions();
|
|
801
|
-
/** The default fan-out coordinator execution bounds. */
|
|
802
1293
|
const defaultFanOutPolicy = AgentPolicy.make({
|
|
803
|
-
maxTurns:
|
|
804
|
-
maxToolCalls:
|
|
805
|
-
maxDuration: "
|
|
806
|
-
toolConcurrency:
|
|
1294
|
+
maxTurns: 7,
|
|
1295
|
+
maxToolCalls: 25,
|
|
1296
|
+
maxDuration: "20 minutes",
|
|
1297
|
+
toolConcurrency: 4,
|
|
807
1298
|
repeatedFailureLimit: 3,
|
|
808
|
-
tokenBudget:
|
|
1299
|
+
tokenBudget: 4e5,
|
|
809
1300
|
contextTokenLimit: 15e4,
|
|
810
1301
|
onExhaustion: "final-answer"
|
|
811
1302
|
});
|
|
812
|
-
const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-
|
|
1303
|
+
const makeFileReviewerDefinition = (options = {}) => Agent.define("pr-review-worker", {
|
|
813
1304
|
input: FileReviewBrief,
|
|
814
1305
|
output: FileReviewReport,
|
|
815
1306
|
instructions: makeFileReviewerInstructions(options),
|
|
816
1307
|
toolkit: FileReviewToolkit,
|
|
817
1308
|
policy: defaultFileReviewerPolicy,
|
|
818
|
-
description: "
|
|
1309
|
+
description: "Perform one bounded discovery or independent candidate-verification pass over host-supplied pull-request evidence.",
|
|
819
1310
|
metadata: {
|
|
820
1311
|
deploymentClass: "E",
|
|
821
|
-
surface: "read-only"
|
|
1312
|
+
surface: "read-only",
|
|
1313
|
+
stage: "discovery-verification"
|
|
822
1314
|
}
|
|
823
1315
|
});
|
|
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
1316
|
const delegationToolFor = (delegation) => delegation.tool.annotate(ToolExecutionClass, "readonly");
|
|
851
1317
|
const makeFanOutReviewerDefinition = (options, delegation) => Agent.define("pr-fanout-reviewer", {
|
|
852
1318
|
input: ReviewMission,
|
|
@@ -854,14 +1320,14 @@ const makeFanOutReviewerDefinition = (options, delegation) => Agent.define("pr-f
|
|
|
854
1320
|
instructions: makeFanOutReviewInstructions(options),
|
|
855
1321
|
toolkit: Toolkit.make(ListReviewUnits, delegationToolFor(delegation)),
|
|
856
1322
|
policy: defaultFanOutPolicy,
|
|
857
|
-
description: "Coordinate
|
|
1323
|
+
description: "Coordinate deterministic general/specialist discovery and independent candidate verification over bounded review units.",
|
|
858
1324
|
metadata: {
|
|
859
1325
|
deploymentClass: "E",
|
|
860
1326
|
surface: "read-only",
|
|
861
|
-
delegation: "S1-attached"
|
|
1327
|
+
delegation: "S1-attached",
|
|
1328
|
+
assurance: "multi-pass"
|
|
862
1329
|
}
|
|
863
1330
|
});
|
|
864
|
-
/** Build one coherent fan-out suite: child, coordinator, and delegation. */
|
|
865
1331
|
const makeFanOutReviewSuite = (options = {}) => {
|
|
866
1332
|
const child = makeFileReviewerDefinition({ guidance: options.guidance });
|
|
867
1333
|
const delegation = makeFileReviewDelegation(child);
|
|
@@ -872,25 +1338,13 @@ const makeFanOutReviewSuite = (options = {}) => {
|
|
|
872
1338
|
};
|
|
873
1339
|
};
|
|
874
1340
|
const defaultSuite = makeFanOutReviewSuite();
|
|
875
|
-
/** The default child Agent Definition. */
|
|
876
1341
|
const FileReviewer = defaultSuite.child;
|
|
877
|
-
/** The default coordinator Agent Definition. */
|
|
878
1342
|
const FanOutReviewer = defaultSuite.parent;
|
|
879
|
-
/** The default delegation over the default child. */
|
|
880
1343
|
const fileReviewDelegation = defaultSuite.delegation;
|
|
881
|
-
/** The default coordinator-facing delegation Tool (first-party contained mode). */
|
|
882
1344
|
const DelegateFileReview = delegationToolFor(fileReviewDelegation);
|
|
883
|
-
/** The default coordinator Toolkit. */
|
|
884
1345
|
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
1346
|
const FileReviewDelegationFailure = fileReviewDelegation.containedFailure;
|
|
891
|
-
/** Runtime wiring: one delegation plus one explicit child Binding. */
|
|
892
1347
|
const fanOutHandlersLayerFor = (delegation) => (childBinding) => SubagentRuntime.layer(delegation, childBinding, { mapChildFailure: mapFileReviewChildFailure });
|
|
893
|
-
/** Runtime wiring over the default delegation, mirroring the leaf example. */
|
|
894
1348
|
const fanOutHandlersLayer = fanOutHandlersLayerFor(fileReviewDelegation);
|
|
895
1349
|
//#endregion
|
|
896
1350
|
//#region src/internal/fingerprint.ts
|
|
@@ -957,10 +1411,12 @@ var StoredReviewConcern = class extends Schema.Class("@effect-agent/pr-review/St
|
|
|
957
1411
|
body: StoredText
|
|
958
1412
|
}) {};
|
|
959
1413
|
/**
|
|
960
|
-
* Versioned state embedded
|
|
961
|
-
* head plus
|
|
962
|
-
*
|
|
963
|
-
*
|
|
1414
|
+
* Versioned state embedded only after complete input assignment and settled
|
|
1415
|
+
* configured review assurance. The head plus full-scope fingerprint forms an
|
|
1416
|
+
* incremental baseline; an absent unresolved item never means the path is
|
|
1417
|
+
* defect-free. The `acceptedScopeFingerprint` name is retained for wire
|
|
1418
|
+
* compatibility. Storing hundreds of path strings separately would not fit
|
|
1419
|
+
* GitHub's bounded review body in the worst case.
|
|
964
1420
|
*/
|
|
965
1421
|
var ReviewState = class extends Schema.Class("@effect-agent/pr-review/ReviewState")({
|
|
966
1422
|
version: Schema.Literal(1),
|
|
@@ -1170,7 +1626,7 @@ const selectReviewRange = (input) => {
|
|
|
1170
1626
|
const selectedFiles = [...selectedByPath.values()].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1171
1627
|
return {
|
|
1172
1628
|
mode: "incremental",
|
|
1173
|
-
reason: `changes since
|
|
1629
|
+
reason: `changes since settled review head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}`,
|
|
1174
1630
|
files: selectedFiles,
|
|
1175
1631
|
affectedPaths: [...affectedPaths].sort(),
|
|
1176
1632
|
totalFiles: selectedFiles.length,
|
|
@@ -1842,6 +2298,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
|
|
|
1842
2298
|
return Option.isSome(latest) && latest.value === current;
|
|
1843
2299
|
});
|
|
1844
2300
|
//#endregion
|
|
1845
|
-
export {
|
|
2301
|
+
export { DiscoveredConcern as $, normalizeRepoRelativePath as $n, ReviewPassId as $t, ReviewStateAuthenticator as A, ReviewConcern as An, defaultFanOutPolicy as At, selectedPullRequestSourceLayer as B, fileReviewEvidenceChunks as Bn, makeFileReviewerInstructions as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, MAX_PATCH_CHARS as Cn, MAX_UNIT_CANDIDATES as Ct, ReviewScopeMode as D, REVIEW_TOOL_RESULT_MAX_BYTES as Dn, ReviewWorkPhase as Dt, ReviewMode as E, PullRequestReviewer as En, ReviewWorkPerspective as Et, buildProfileMission as F, ReviewVerdict as Fn, fileReviewDelegation as Ft, webCryptoReviewStateAuthenticatorLayer as G, resolveGuidance as Gn, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as Gt, toStoredFinding as H, makeReviewInstructions as Hn, reviewCandidateSubjectKey as Ht, computeProfileFingerprint as I, WalkthroughEntry as In, fileReviewPolicy as It, extractFingerprint as J, MAX_FILE_CHARS as Jn, MAX_UNIT_FILES as Jt, FINGERPRINT_MARKER_LENGTH as K, reviewInstructions as Kn, MAX_REVIEW_UNITS as Kt, fromStoredConcern as L, clampMaxFindings as Ln, fileReviewerInstructions as Lt, ReviewStateMarkerTooLarge as M, ReviewMission as Mn, fanOutHandlersLayer as Mt, StoredReviewConcern as N, ReviewToolkit as Nn, fanOutHandlersLayerFor as Nt, ReviewState as O, ReadFile as On, assessmentSettlesSuggestionExactly as Ot, StoredReviewFinding as P, ReviewToolkitLayer as Pn, fanOutReviewInstructions as Pt, DelegateFileReview as Q, ReviewInputViolation as Qn, ReviewEvidenceShardId as Qt, fromStoredFinding as R, defaultReviewPolicy as Rn, makeFanOutReviewInstructions as Rt, GitCommitSha as S, MAX_FINDINGS as Sn, MAX_REVIEW_CHILDREN as St, ReviewHeadComparison as T, MAX_WALKTHROUGH_SUMMARY_CHARS as Tn, ReviewCandidateId as Tt, unavailableReviewStateAuthenticatorLayer as U, readFileDiffHandler as Un, MAX_FILE_EVIDENCE_CHARS as Ut, toStoredConcern as V, listChangedFilesHandler as Vn, mapFileReviewChildFailure as Vt, validateReviewState as W, readFileHandler as Wn, MAX_MERGED_FINDINGS as Wt, CandidateAssessment as X, PullRequestSource as Xn, ReviewDiscoveryPerspective as Xt, renderFingerprintMarker as Y, PullRequestMetadata as Yn, ReviewDiscoveryPass as Yt, ConcernCandidate as Z, PullRequestSourceFailure as Zn, ReviewEvidenceShard as Zt, ReviewRetirementHost as _, FindingCategory as _n, ListReviewUnits as _t, PriorReviews as a, UNIT_EVIDENCE_CHAR_BUDGET as an, annotatePatch as ar, FileReviewDelegationFailure as at, hasReviewMetadataMarker as b, ListChangedFilesQuery as bn, MAX_CHILD_FINDINGS as bt, fingerprintUnchanged as c, planReviewUnits as cn, isReviewableFile as cr, FileReviewReport as ct, gitHubReviewPublisherLayer as d, ChangedFilesView as dn, FileReviewToolkitLayer as dt, ReviewRiskCategory as en, anchorViolation as er, FanOutCoordinatorToolkit as et, gitHubReviewRetirementHostLayer as f, CodeReview as fn, FileReviewUnitFailed as ft, ReviewRetirementFailure as g, FileSliceQuery as gn, FindingCandidate as gt, RetirableReviewComment as h, FileSlice as hn, FileReviewer as ht, PriorReviewLookupFailure as i, UNIT_CHANGED_LINE_BUDGET as in, MAX_REVIEW_CONTENT_CHARS as ir, FileReviewBrief as it, ReviewStateMarker as j, ReviewFinding as jn, defaultFileReviewerPolicy as jt, ReviewStateAuthenticationFailure as k, ReadFileDiff as kn, confirmedFindingForPublication as kt, gitHubPriorReviewsLayer as l, rankAndDedupeFindings as ln, parsePatch as lr, FileReviewRequest as lt, RetirableReview as m, FileDiffView as mn, FileReviewWorkRejected as mt, GitHubApiFailure as n, ReviewUnitId as nn, ChangedFileStatus as nr, FanOutReviewToolkit as nt, PublishedReview as o, classifyReviewRisks as on, commentableLines as or, FileReviewEvidence as ot, parseGitHubSubmittedAt as p, FileDiffQuery as pn, FileReviewUnitResult as pt, computeChangesetFingerprint as q, MAX_CHANGED_FILES as qn, MAX_UNIT_EVIDENCE_SHARDS as qt, GitHubReviewTarget as r, ReviewUnitPlan as rn, ChangedPath as rr, FanOutReviewer as rt, ReviewPublisher as s, findingAnchorInUnitEvidence as sn, hasReviewableContent as sr, FileReviewFailure as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, ReviewUnit as tn, ChangedFile as tr, FanOutCoordinatorToolkitLayer as tt, gitHubPullRequestSourceLayer as u, ChangedFileSummary as un, renderReviewContent as ur, FileReviewToolkit as ut, ReviewRetirementReport as v, FindingSeverity as vn, ListReviewUnitsQuery as vt, ReviewExecutionContext as w, MAX_WALKTHROUGH_ENTRIES as wn, ReviewCandidate as wt, retireStaleReviews as x, MAX_CONCERNS as xn, MAX_FILE_REVIEW_TOOL_CALLS as xt, decideReviewRetirement as y, ListChangedFiles as yn, MAX_CHILD_CONCERNS as yt, selectReviewRange as z, fileDiffView as zn, makeFanOutReviewSuite as zt };
|
|
1846
2302
|
|
|
1847
|
-
//# sourceMappingURL=github-
|
|
2303
|
+
//# sourceMappingURL=github-BgtP7Rdv.mjs.map
|