@effect-agent/pr-review 0.1.0-beta.25 → 0.1.0-beta.27

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.
@@ -670,13 +670,13 @@ const adjudicationIdentity = (adjudication) => adjudication.path !== void 0 && a
670
670
  const MAX_STORED_UNREVIEWED_PATHS = 100;
671
671
  /** Failed-pass records stored beside the leftover paths; one per unit stage. */
672
672
  const MAX_STORED_UNREVIEWED_PASSES = 24;
673
- /** Stages a leftover path may need retried without a second general discovery. */
673
+ /** Stages a leftover path may need retried on the next incremental run. */
674
674
  const UnreviewedStage = Schema.Literals([
675
675
  "discovery",
676
676
  "specialist",
677
677
  "verification"
678
678
  ]);
679
- /** One failed fan-out pass whose paths should be retried, not rediscovered. */
679
+ /** One failed fan-out pass whose stage remains attached to its exact paths. */
680
680
  var StoredUnreviewedPass = class extends Schema.Class("@effect-agent/pr-review/StoredUnreviewedPass")({
681
681
  stage: UnreviewedStage,
682
682
  paths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12))
@@ -856,11 +856,25 @@ var ReviewHeadComparison = class extends Schema.Class("@effect-agent/pr-review/R
856
856
  /** GitHub caps compare-file payloads at 300; equality is conservatively truncated. */
857
857
  truncated: Schema.Boolean
858
858
  }) {};
859
+ /**
860
+ * Current and previous paths for 300 PR files plus bounded stored continuity
861
+ * paths. The live adapter refuses a larger snapshot-comparison request.
862
+ */
863
+ const MAX_TREE_COMPARISON_PATHS = 750;
864
+ /** A direct comparison of two complete commit tree snapshots. */
865
+ var ReviewTreeComparison = class extends Schema.Class("@effect-agent/pr-review/ReviewTreeComparison")({
866
+ baseSha: GitCommitSha,
867
+ headSha: GitCommitSha,
868
+ changedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(750)),
869
+ /** True when GitHub returned either recursive tree incompletely. */
870
+ truncated: Schema.Boolean
871
+ }) {};
859
872
  const fullReviewSelection = (input) => ({
860
873
  mode: "full",
861
874
  reason: input.reason,
862
875
  files: input.files,
863
876
  affectedPaths: input.files.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]),
877
+ retryPasses: [],
864
878
  retryPaths: [],
865
879
  retryStages: [],
866
880
  totalFiles: input.totalFiles,
@@ -886,7 +900,7 @@ const validateReviewState = (state, current, profileFingerprint) => {
886
900
  const filePaths = (file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath];
887
901
  const incrementalFromDelta = (input) => {
888
902
  const currentPaths = new Set(input.fullFiles.flatMap(filePaths));
889
- const affectedPaths = /* @__PURE__ */ new Set([...input.deltaFiles.flatMap(filePaths), ...input.extraAffectedPaths ?? []]);
903
+ const affectedPaths = /* @__PURE__ */ new Set([...input.deltaPaths, ...input.extraAffectedPaths ?? []]);
890
904
  const initialAffectedCount = affectedPaths.size;
891
905
  let expanded = true;
892
906
  while (expanded) {
@@ -901,29 +915,43 @@ const incrementalFromDelta = (input) => {
901
915
  }
902
916
  }
903
917
  const selectedByPath = /* @__PURE__ */ new Map();
904
- for (const file of input.deltaFiles) if (currentPaths.has(file.path) || file.previousPath !== void 0 && currentPaths.has(file.previousPath)) selectedByPath.set(file.path, file);
905
918
  const carriedPaths = input.priorState.unreviewedPaths.filter((path) => currentPaths.has(path));
906
919
  const retryOnly = /* @__PURE__ */ new Set();
907
- const retryStages = /* @__PURE__ */ new Set();
908
920
  for (const path of carriedPaths) {
909
921
  if (affectedPaths.has(path)) continue;
910
922
  retryOnly.add(path);
911
- for (const pass of input.priorState.unreviewedPasses) if (pass.paths.includes(path)) retryStages.add(pass.stage);
912
923
  }
913
- if (retryStages.has("verification") && !retryStages.has("discovery") && !retryStages.has("specialist")) {
914
- retryStages.add("discovery");
915
- retryStages.add("specialist");
924
+ const retryPathsByStage = /* @__PURE__ */ new Map();
925
+ const representedRetryPaths = /* @__PURE__ */ new Set();
926
+ for (const pass of input.priorState.unreviewedPasses) for (const path of pass.paths) {
927
+ if (!retryOnly.has(path)) continue;
928
+ const paths = retryPathsByStage.get(pass.stage) ?? /* @__PURE__ */ new Set();
929
+ paths.add(path);
930
+ retryPathsByStage.set(pass.stage, paths);
931
+ representedRetryPaths.add(path);
916
932
  }
917
- if (retryOnly.size > 0 && retryStages.size === 0) {
918
- for (const path of retryOnly) affectedPaths.add(path);
919
- retryStages.add("discovery");
920
- retryStages.add("specialist");
921
- retryStages.add("verification");
933
+ for (const path of retryOnly) {
934
+ if (representedRetryPaths.has(path)) continue;
935
+ retryOnly.delete(path);
936
+ affectedPaths.add(path);
922
937
  }
938
+ const retryPasses = [
939
+ "discovery",
940
+ "specialist",
941
+ "verification"
942
+ ].flatMap((stage) => {
943
+ const paths = [...retryPathsByStage.get(stage) ?? []].filter((path) => retryOnly.has(path)).sort();
944
+ return Array.from({ length: Math.ceil(paths.length / 12) }, (_, index) => StoredUnreviewedPass.make({
945
+ stage,
946
+ paths: paths.slice(index * 12, (index + 1) * 12)
947
+ }));
948
+ });
949
+ const retryPaths = [...retryOnly].sort();
950
+ const retryStages = [...new Set(retryPasses.map((pass) => pass.stage))];
923
951
  for (const file of input.fullFiles) if (affectedPaths.has(file.path) || file.previousPath !== void 0 && affectedPaths.has(file.previousPath) || retryOnly.has(file.path) || file.previousPath !== void 0 && retryOnly.has(file.previousPath)) selectedByPath.set(file.path, file);
924
952
  const selectedFiles = [...selectedByPath.values()].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
925
- const leftoverCount = [...retryOnly].filter((path) => !affectedPaths.has(path)).length;
926
- const carriedReason = leftoverCount > 0 ? `; retrying ${leftoverCount} unchanged leftover path(s) without rediscovery` : carriedPaths.length > 0 ? `; retrying ${carriedPaths.length} carried unreviewed path(s)` : "";
953
+ const leftoverCount = retryPaths.length;
954
+ const carriedReason = leftoverCount > 0 ? `; retrying ${leftoverCount} unchanged leftover path(s) by recorded failed stage` : carriedPaths.length > 0 ? `; retrying ${carriedPaths.length} carried unreviewed path(s)` : "";
927
955
  const concernPathCount = affectedPaths.size - initialAffectedCount;
928
956
  const concernReason = concernPathCount === 0 ? "" : `; reopening ${concernPathCount} related concern path(s) for context`;
929
957
  return {
@@ -931,8 +959,9 @@ const incrementalFromDelta = (input) => {
931
959
  reason: `${input.reason}${carriedReason}${concernReason}`,
932
960
  files: selectedFiles,
933
961
  affectedPaths: [...affectedPaths].sort(),
934
- retryPaths: [...retryOnly].filter((path) => !affectedPaths.has(path)).sort(),
935
- retryStages: [...retryStages].sort(),
962
+ retryPasses,
963
+ retryPaths,
964
+ retryStages,
936
965
  totalFiles: selectedFiles.length,
937
966
  baselineSha: input.priorState.reviewedHeadSha,
938
967
  priorState: input.priorState,
@@ -964,24 +993,27 @@ const selectReviewRange = (input) => {
964
993
  baseReason = `; base advanced from ${input.priorState.baseSha.slice(0, 7)} and overlapping PR paths were included`;
965
994
  }
966
995
  return incrementalFromDelta({
967
- current: input.current,
968
996
  fullFiles: input.fullFiles,
969
997
  profileFingerprint: input.profileFingerprint,
970
998
  priorState: input.priorState,
971
- deltaFiles: comparison.files,
999
+ deltaPaths: comparison.files.flatMap(filePaths),
972
1000
  extraAffectedPaths: extraAffected,
973
1001
  reason: `changes since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}`
974
1002
  });
975
1003
  }
976
1004
  const contentComparison = input.contentComparison;
977
- if (contentComparison !== void 0 && !contentComparison.truncated) return incrementalFromDelta({
978
- current: input.current,
979
- fullFiles: input.fullFiles,
980
- profileFingerprint: input.profileFingerprint,
981
- priorState: input.priorState,
982
- deltaFiles: contentComparison.files,
983
- reason: `rewritten history; contents changed since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}`
984
- });
1005
+ if (contentComparison !== void 0) {
1006
+ if (contentComparison.baseSha !== input.priorState.reviewedHeadSha || contentComparison.headSha !== input.current.headSha) return full("the rewritten-head tree snapshot comparison did not match the requested heads");
1007
+ if (contentComparison.truncated) return full("the rewritten-head tree snapshot comparison was truncated");
1008
+ return incrementalFromDelta({
1009
+ fullFiles: input.fullFiles,
1010
+ profileFingerprint: input.profileFingerprint,
1011
+ priorState: input.priorState,
1012
+ deltaPaths: contentComparison.changedPaths,
1013
+ reason: `rewritten history; contents changed since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}`
1014
+ });
1015
+ }
1016
+ if (input.contentComparisonFailure !== void 0) return full(`the rewritten-head tree snapshot comparison failed: ${input.contentComparisonFailure.slice(0, 2048)}`);
985
1017
  if (comparison === void 0) return full("the incremental head comparison was unavailable");
986
1018
  if (comparison.truncated) return full("the incremental comparison exceeded GitHub's file bound");
987
1019
  return full("the prior reviewed head is not an ancestor of the current head");
@@ -2520,9 +2552,26 @@ const remapPlanUnitIds = (plan, offset) => {
2520
2552
  });
2521
2553
  };
2522
2554
  const scheduleFanOutWork = (input) => {
2523
- const retryPathSet = new Set(input.retry?.paths ?? []);
2524
- const retryStages = new Set(input.retry?.stages ?? []);
2525
- if (retryPathSet.size === 0) {
2555
+ const requestedRetryPasses = input.retry?.passes ?? input.retry?.stages?.map((stage) => ({
2556
+ stage,
2557
+ paths: input.retry?.paths ?? []
2558
+ })) ?? [];
2559
+ const requestedStagesByPath = /* @__PURE__ */ new Map();
2560
+ for (const pass of requestedRetryPasses) for (const path of pass.paths) {
2561
+ const stages = requestedStagesByPath.get(path) ?? /* @__PURE__ */ new Set();
2562
+ stages.add(pass.stage);
2563
+ requestedStagesByPath.set(path, stages);
2564
+ }
2565
+ const canonicalPathByKnownPath = /* @__PURE__ */ new Map();
2566
+ const retryStagesByPath = /* @__PURE__ */ new Map();
2567
+ for (const file of input.files) {
2568
+ canonicalPathByKnownPath.set(file.path, file.path);
2569
+ if (file.previousPath !== void 0) canonicalPathByKnownPath.set(file.previousPath, file.path);
2570
+ const requested = [requestedStagesByPath.get(file.path), ...file.previousPath === void 0 ? [] : [requestedStagesByPath.get(file.previousPath)]];
2571
+ const stages = new Set(requested.flatMap((entry) => [...entry ?? []]));
2572
+ if (stages.size > 0) retryStagesByPath.set(file.path, stages);
2573
+ }
2574
+ if (retryStagesByPath.size === 0) {
2526
2575
  const plan = planReviewUnits(input.files, { totalChangedFiles: input.totalChangedFiles });
2527
2576
  const passesByUnit = /* @__PURE__ */ new Map();
2528
2577
  for (const pass of plan.discoveryPasses) {
@@ -2533,39 +2582,67 @@ const scheduleFanOutWork = (input) => {
2533
2582
  return {
2534
2583
  plan,
2535
2584
  passesByUnit,
2536
- overflowRetryPaths: []
2585
+ overflowRetryPasses: []
2537
2586
  };
2538
2587
  }
2539
- const freshFiles = input.files.filter((file) => !retryPathSet.has(file.path));
2540
- const retryFiles = input.files.filter((file) => retryPathSet.has(file.path));
2541
- const freshPlan = planReviewUnits(freshFiles, { totalChangedFiles: input.totalChangedFiles });
2542
- const retryPlan = remapPlanUnitIds(planReviewUnits(retryFiles, { totalChangedFiles: input.totalChangedFiles }), freshPlan.units.length);
2543
- const acceptedFresh = freshPlan.units.slice(0, 8);
2544
- const acceptedRetry = retryPlan.units.slice(0, Math.max(0, 8 - acceptedFresh.length));
2545
- const overflowRetryPaths = retryPlan.units.slice(acceptedRetry.length).flatMap((unit) => [...unit.paths]);
2546
- const retryPassFilter = (pass) => {
2547
- const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
2548
- return retryStages.has(stage);
2588
+ const discoveryStagesFor = (stages) => {
2589
+ if (stages.has("verification")) return ["discovery", "specialist"];
2590
+ return [...stages.has("discovery") ? ["discovery"] : [], ...stages.has("specialist") ? ["specialist"] : []];
2549
2591
  };
2550
- const acceptedRetryIds = new Set(acceptedRetry.map((unit) => unit.unitId));
2551
- let retryPasses = retryPlan.discoveryPasses.filter((pass) => acceptedRetryIds.has(pass.unitId) && retryPassFilter(pass));
2552
- if (retryStages.has("verification") && !retryStages.has("discovery") && !retryStages.has("specialist")) retryPasses = retryPlan.discoveryPasses.filter((pass) => acceptedRetryIds.has(pass.unitId));
2553
- const acceptedFreshIds = new Set(acceptedFresh.map((unit) => unit.unitId));
2554
- const discoveryPasses = [...freshPlan.discoveryPasses.filter((pass) => acceptedFreshIds.has(pass.unitId)), ...retryPasses];
2592
+ const freshFiles = input.files.filter((file) => !retryStagesByPath.has(file.path));
2593
+ const retryGroups = /* @__PURE__ */ new Map();
2594
+ for (const file of input.files) {
2595
+ const retryStages = retryStagesByPath.get(file.path);
2596
+ if (retryStages === void 0) continue;
2597
+ const stages = discoveryStagesFor(retryStages);
2598
+ const key = stages.join("|");
2599
+ const group = retryGroups.get(key) ?? {
2600
+ stages,
2601
+ files: []
2602
+ };
2603
+ group.files.push(file);
2604
+ retryGroups.set(key, group);
2605
+ }
2606
+ const batches = [...freshFiles.length === 0 ? [] : [{
2607
+ stages: ["discovery", "specialist"],
2608
+ files: freshFiles
2609
+ }], ...[...retryGroups.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, group]) => group)];
2610
+ const subplans = [];
2611
+ const acceptedUnits = [];
2612
+ const rejectedUnits = [];
2613
+ const discoveryPasses = [];
2614
+ for (const batch of batches) {
2615
+ const batchPlan = remapPlanUnitIds(planReviewUnits(batch.files, { totalChangedFiles: batch.files.length }), acceptedUnits.length);
2616
+ subplans.push(batchPlan);
2617
+ const accepted = batchPlan.units.slice(0, Math.max(0, 8 - acceptedUnits.length));
2618
+ acceptedUnits.push(...accepted);
2619
+ rejectedUnits.push(...batchPlan.units.slice(accepted.length));
2620
+ const acceptedIds = new Set(accepted.map((unit) => unit.unitId));
2621
+ discoveryPasses.push(...batchPlan.discoveryPasses.filter((pass) => {
2622
+ const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
2623
+ return acceptedIds.has(pass.unitId) && batch.stages.includes(stage);
2624
+ }));
2625
+ }
2626
+ const acceptedPaths = new Set(acceptedUnits.flatMap((unit) => unit.paths));
2627
+ const incompletePlannedPaths = /* @__PURE__ */ new Set([
2628
+ ...subplans.flatMap((plan) => plan.partialEvidencePaths),
2629
+ ...subplans.flatMap((plan) => plan.unassignedPaths),
2630
+ ...rejectedUnits.flatMap((unit) => unit.paths)
2631
+ ]);
2632
+ const partialEvidencePaths = [...incompletePlannedPaths].filter((path) => acceptedPaths.has(path)).sort();
2633
+ const unassignedPaths = [...incompletePlannedPaths].filter((path) => !acceptedPaths.has(path)).sort();
2634
+ const rejectedEvidenceShards = rejectedUnits.flatMap((unit) => unit.evidenceShards);
2635
+ const undiffablePaths = [...new Set(subplans.flatMap((plan) => plan.undiffablePaths))].sort();
2555
2636
  const plan = ReviewUnitPlan.make({
2556
2637
  totalFiles: input.files.length,
2557
- truncated: freshPlan.truncated || retryPlan.truncated,
2558
- units: [...acceptedFresh, ...acceptedRetry],
2638
+ truncated: input.files.length < input.totalChangedFiles,
2639
+ units: acceptedUnits,
2559
2640
  discoveryPasses,
2560
- undiffablePaths: [.../* @__PURE__ */ new Set([...freshPlan.undiffablePaths, ...retryPlan.undiffablePaths])].sort(),
2561
- partialEvidencePaths: [.../* @__PURE__ */ new Set([...freshPlan.partialEvidencePaths, ...retryPlan.partialEvidencePaths])].sort(),
2562
- unassignedEvidenceShardCount: freshPlan.unassignedEvidenceShardCount + retryPlan.unassignedEvidenceShardCount,
2563
- unassignedEvidenceShardIds: [...freshPlan.unassignedEvidenceShardIds, ...retryPlan.unassignedEvidenceShardIds].slice(0, 96),
2564
- unassignedPaths: [.../* @__PURE__ */ new Set([
2565
- ...freshPlan.unassignedPaths,
2566
- ...retryPlan.unassignedPaths,
2567
- ...overflowRetryPaths
2568
- ])].sort()
2641
+ undiffablePaths,
2642
+ partialEvidencePaths,
2643
+ unassignedEvidenceShardCount: subplans.reduce((total, item) => total + item.unassignedEvidenceShardCount, 0) + rejectedEvidenceShards.length,
2644
+ unassignedEvidenceShardIds: [...subplans.flatMap((item) => item.unassignedEvidenceShardIds), ...rejectedEvidenceShards.map((shard) => shard.shardId)].slice(0, 96),
2645
+ unassignedPaths
2569
2646
  });
2570
2647
  const passesByUnit = /* @__PURE__ */ new Map();
2571
2648
  for (const pass of discoveryPasses) {
@@ -2573,10 +2650,33 @@ const scheduleFanOutWork = (input) => {
2573
2650
  passes.push(pass);
2574
2651
  passesByUnit.set(pass.unitId, passes);
2575
2652
  }
2653
+ const incompletePaths = /* @__PURE__ */ new Set([
2654
+ ...partialEvidencePaths,
2655
+ ...unassignedPaths,
2656
+ ...undiffablePaths
2657
+ ]);
2658
+ const overflowRetryPathsByStage = /* @__PURE__ */ new Map();
2659
+ for (const pass of requestedRetryPasses) for (const path of pass.paths) {
2660
+ const canonicalPath = canonicalPathByKnownPath.get(path);
2661
+ if (canonicalPath === void 0 || !incompletePaths.has(canonicalPath)) continue;
2662
+ const paths = overflowRetryPathsByStage.get(pass.stage) ?? /* @__PURE__ */ new Set();
2663
+ paths.add(canonicalPath);
2664
+ overflowRetryPathsByStage.set(pass.stage, paths);
2665
+ }
2576
2666
  return {
2577
2667
  plan,
2578
2668
  passesByUnit,
2579
- overflowRetryPaths
2669
+ overflowRetryPasses: [
2670
+ "discovery",
2671
+ "specialist",
2672
+ "verification"
2673
+ ].flatMap((stage) => {
2674
+ const paths = [...overflowRetryPathsByStage.get(stage) ?? []].sort();
2675
+ return Array.from({ length: Math.ceil(paths.length / 12) }, (_, index) => ({
2676
+ stage,
2677
+ paths: paths.slice(index * 12, (index + 1) * 12)
2678
+ }));
2679
+ })
2580
2680
  };
2581
2681
  };
2582
2682
  /**
@@ -2586,7 +2686,7 @@ const scheduleFanOutWork = (input) => {
2586
2686
  * only. The verdict is derived from confirmed severities, never model prose.
2587
2687
  */
2588
2688
  const runFanOutReview = (binding, input) => Effect.gen(function* () {
2589
- const { plan, passesByUnit, overflowRetryPaths } = scheduleFanOutWork(input);
2689
+ const { plan, passesByUnit, overflowRetryPasses } = scheduleFanOutWork(input);
2590
2690
  const outcomes = yield* Effect.forEach(plan.units, (unit) => reviewUnit(binding, unit, passesByUnit.get(unit.unitId) ?? [], input), { concurrency: 4 });
2591
2691
  const failedPasses = outcomes.flatMap((outcome) => outcome.failedPasses);
2592
2692
  const unsettledCandidates = outcomes.reduce((total, outcome) => total + outcome.unsettledCandidates, 0);
@@ -2634,14 +2734,7 @@ const runFanOutReview = (binding, input) => Effect.gen(function* () {
2634
2734
  ...plan.partialEvidencePaths,
2635
2735
  ...plan.undiffablePaths
2636
2736
  ])].sort(),
2637
- unreviewedPasses: [...outcomes.flatMap((outcome) => outcome.unreviewedPasses), ...overflowRetryPaths.length === 0 ? [] : (input.retry?.stages.length ? input.retry.stages : [
2638
- "discovery",
2639
- "specialist",
2640
- "verification"
2641
- ]).map((stage) => ({
2642
- stage,
2643
- paths: overflowRetryPaths
2644
- }))],
2737
+ unreviewedPasses: [...outcomes.flatMap((outcome) => outcome.unreviewedPasses), ...overflowRetryPasses],
2645
2738
  turns: outcomes.reduce((total, outcome) => total + outcome.turns, 0)
2646
2739
  };
2647
2740
  });
@@ -3183,6 +3276,41 @@ const GitHubCompareWire = Schema.Struct({
3183
3276
  merge_base_commit: Schema.Struct({ sha: Schema.String }),
3184
3277
  files: GitHubFilesPageWire
3185
3278
  });
3279
+ const GitHubGitCommitWire = Schema.Struct({
3280
+ sha: GitCommitSha,
3281
+ tree: Schema.Struct({ sha: GitCommitSha })
3282
+ });
3283
+ const GitHubTreeEntryFields = {
3284
+ path: Schema.String.check(Schema.isMaxLength(4096)),
3285
+ sha: GitCommitSha
3286
+ };
3287
+ const GitHubTreeEntryWire = Schema.Union([
3288
+ Schema.Struct({
3289
+ ...GitHubTreeEntryFields,
3290
+ mode: Schema.Literals([
3291
+ "100644",
3292
+ "100755",
3293
+ "120000"
3294
+ ]),
3295
+ type: Schema.Literal("blob")
3296
+ }),
3297
+ Schema.Struct({
3298
+ ...GitHubTreeEntryFields,
3299
+ mode: Schema.Literal("040000"),
3300
+ type: Schema.Literal("tree")
3301
+ }),
3302
+ Schema.Struct({
3303
+ ...GitHubTreeEntryFields,
3304
+ mode: Schema.Literal("160000"),
3305
+ type: Schema.Literal("commit")
3306
+ })
3307
+ ]);
3308
+ const GitHubTreeWire = Schema.Struct({
3309
+ sha: GitCommitSha,
3310
+ tree: Schema.Array(GitHubTreeEntryWire).check(Schema.isMaxLength(1e5)),
3311
+ truncated: Schema.Boolean
3312
+ });
3313
+ const TreeComparisonPaths = Schema.Array(ChangedPath).check(Schema.isMaxLength(750));
3186
3314
  /** Reviews are paged chronologically; scanning stays bounded. */
3187
3315
  const MAX_PRIOR_REVIEW_PAGES = 5;
3188
3316
  /** GitHub-backed PriorReviews over the pull-request reviews endpoint. */
@@ -3193,6 +3321,11 @@ const gitHubPriorReviewsLayer = Layer.effect(PriorReviews)(Effect.gen(function*
3193
3321
  const reviewAuthorLogin = target.reviewAuthorLogin ?? "github-actions[bot]";
3194
3322
  const decodePage = Schema.decodeUnknownEffect(GitHubPriorReviewsPageWire);
3195
3323
  const asLookupFailure = (error) => PriorReviewLookupFailure.make({ reason: `${error._tag}: ${error.message ?? "request failed"}`.slice(0, 2048) });
3324
+ const asTreeLookupFailure = (operation) => (error) => PriorReviewLookupFailure.make({ reason: `${operation}: ${error._tag}: ${error.message ?? "request failed"}`.slice(0, 2048) });
3325
+ const decodeLookupJson = (schema, operation) => {
3326
+ const decode = Schema.decodeUnknownEffect(schema);
3327
+ return (response) => response.json.pipe(Effect.mapError(asTreeLookupFailure(operation)), Effect.flatMap((body) => decode(body).pipe(Effect.mapError(asTreeLookupFailure(operation)))));
3328
+ };
3196
3329
  const readMarkers = (authenticator) => Effect.gen(function* () {
3197
3330
  const perPage = 100;
3198
3331
  let latest = Option.none();
@@ -3219,8 +3352,8 @@ const gitHubPriorReviewsLayer = Layer.effect(PriorReviews)(Effect.gen(function*
3219
3352
  latestState
3220
3353
  };
3221
3354
  }).pipe(Effect.provideService(HttpClient.HttpClient, client));
3222
- const compareCommits = (baseSha, headSha, separator) => Effect.gen(function* () {
3223
- const wire = yield* (yield* HttpClient.execute(withCommonHeaders(HttpClientRequest.get(`${target.apiUrl}/repos/${target.repository}/compare/${encodeURIComponent(baseSha)}${separator}${encodeURIComponent(headSha)}`).pipe(HttpClientRequest.acceptJson), target.token)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(asLookupFailure))).json.pipe(Effect.mapError(asLookupFailure), Effect.flatMap((body) => Schema.decodeUnknownEffect(GitHubCompareWire)(body).pipe(Effect.mapError(asLookupFailure))));
3355
+ const compareCommits = (baseSha, headSha) => Effect.gen(function* () {
3356
+ const wire = yield* (yield* HttpClient.execute(withCommonHeaders(HttpClientRequest.get(`${target.apiUrl}/repos/${target.repository}/compare/${encodeURIComponent(baseSha)}...${encodeURIComponent(headSha)}`).pipe(HttpClientRequest.acceptJson), target.token)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(asLookupFailure))).json.pipe(Effect.mapError(asLookupFailure), Effect.flatMap((body) => Schema.decodeUnknownEffect(GitHubCompareWire)(body).pipe(Effect.mapError(asLookupFailure))));
3224
3357
  const files = wire.files.map(toChangedFile);
3225
3358
  return ReviewHeadComparison.make({
3226
3359
  status: wire.status,
@@ -3231,21 +3364,61 @@ const gitHubPriorReviewsLayer = Layer.effect(PriorReviews)(Effect.gen(function*
3231
3364
  truncated: files.length >= 300
3232
3365
  });
3233
3366
  }).pipe(Effect.provideService(HttpClient.HttpClient, client));
3234
- const compareTrees = (baseSha, headSha) => compareCommits(baseSha, headSha, "..").pipe(Effect.map((comparison) => ReviewHeadComparison.make({
3235
- status: comparison.status === "identical" ? "identical" : "ahead",
3236
- baseSha,
3237
- headSha,
3238
- mergeBaseSha: baseSha,
3239
- files: comparison.files,
3240
- truncated: comparison.truncated
3241
- })));
3367
+ const readTreeSnapshot = Effect.fn("PriorReviews.readTreeSnapshot")(function* (commitSha) {
3368
+ const commitResponse = yield* client.execute(withCommonHeaders(HttpClientRequest.get(`${target.apiUrl}/repos/${target.repository}/git/commits/${encodeURIComponent(commitSha)}`).pipe(HttpClientRequest.acceptJson), target.token)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(asTreeLookupFailure("get Git commit")));
3369
+ const commit = yield* decodeLookupJson(GitHubGitCommitWire, "decode Git commit")(commitResponse);
3370
+ if (commit.sha !== commitSha) return yield* PriorReviewLookupFailure.make({ reason: `GitHub returned commit ${commit.sha} for requested snapshot ${commitSha}` });
3371
+ const treeResponse = yield* client.execute(withCommonHeaders(HttpClientRequest.get(`${target.apiUrl}/repos/${target.repository}/git/trees/${encodeURIComponent(commit.tree.sha)}`).pipe(HttpClientRequest.acceptJson, HttpClientRequest.setUrlParams({ recursive: "1" })), target.token)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.mapError(asTreeLookupFailure("get recursive Git tree")));
3372
+ const tree = yield* decodeLookupJson(GitHubTreeWire, "decode recursive Git tree")(treeResponse);
3373
+ if (tree.sha !== commit.tree.sha) return yield* PriorReviewLookupFailure.make({ reason: `GitHub returned tree ${tree.sha} for requested tree ${commit.tree.sha}` });
3374
+ const entries = /* @__PURE__ */ new Map();
3375
+ for (const entry of tree.tree) {
3376
+ if (entries.has(entry.path)) return yield* PriorReviewLookupFailure.make({ reason: `GitHub returned duplicate path '${entry.path}' in tree ${tree.sha}` });
3377
+ entries.set(entry.path, entry);
3378
+ }
3379
+ return {
3380
+ entries,
3381
+ truncated: tree.truncated
3382
+ };
3383
+ });
3384
+ const compareTrees = Effect.fn("PriorReviews.compareTrees")(function* (baseSha, headSha, paths) {
3385
+ const decodeSha = Schema.decodeUnknownEffect(GitCommitSha);
3386
+ const [validatedBaseSha, validatedHeadSha, validatedPaths] = yield* Effect.all([
3387
+ decodeSha(baseSha),
3388
+ decodeSha(headSha),
3389
+ Schema.decodeUnknownEffect(TreeComparisonPaths)(paths)
3390
+ ]).pipe(Effect.mapError(asTreeLookupFailure("validate tree comparison request")));
3391
+ const uniquePaths = [...new Set(validatedPaths)].sort();
3392
+ const { base, head } = yield* Effect.all({
3393
+ base: readTreeSnapshot(validatedBaseSha),
3394
+ head: readTreeSnapshot(validatedHeadSha)
3395
+ }, { concurrency: 2 });
3396
+ if (base.truncated || head.truncated) return ReviewTreeComparison.make({
3397
+ baseSha: validatedBaseSha,
3398
+ headSha: validatedHeadSha,
3399
+ changedPaths: [],
3400
+ truncated: true
3401
+ });
3402
+ const changedPaths = uniquePaths.filter((path) => {
3403
+ const before = base.entries.get(path);
3404
+ const after = head.entries.get(path);
3405
+ if (before === void 0 || after === void 0) return before !== after;
3406
+ return before.sha !== after.sha || before.mode !== after.mode || before.type !== after.type;
3407
+ });
3408
+ return ReviewTreeComparison.make({
3409
+ baseSha: validatedBaseSha,
3410
+ headSha: validatedHeadSha,
3411
+ changedPaths,
3412
+ truncated: false
3413
+ });
3414
+ });
3242
3415
  return PriorReviews.of({
3243
3416
  latestFingerprint: readMarkers(Option.none()).pipe(Effect.map((markers) => markers.latestFingerprint)),
3244
3417
  latestState: Effect.gen(function* () {
3245
3418
  const authenticator = yield* ReviewStateAuthenticator;
3246
3419
  return yield* readMarkers(Option.some(authenticator)).pipe(Effect.map((markers) => markers.latestState));
3247
3420
  }),
3248
- compareHeads: (baseSha, headSha) => compareCommits(baseSha, headSha, "..."),
3421
+ compareHeads: compareCommits,
3249
3422
  compareTrees
3250
3423
  });
3251
3424
  }));
@@ -3259,6 +3432,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
3259
3432
  return Option.isSome(latest) && latest.value === current;
3260
3433
  });
3261
3434
  //#endregion
3262
- export { ReviewDiscoveryPass as $, ReviewFinding as $n, MAX_STORED_ADJUDICATIONS as $t, MAX_CHILD_FINDINGS as A, validateReviewState as An, parsePatch as Ar, buildPriorReviewContext as At, assessmentSettlesSuggestionExactly as B, FindingSeverity as Bn, threadFindingTarget as Bt, FileReviewBrief as C, fullReviewSelection as Cn, ChangedFileStatus as Cr, anchorViolation as Ct, FileReviewer as D, toStoredConcern as Dn, commentableLines as Dr, MAX_THREAD_ADJUDICATION_COMMANDS as Dt, FileReviewToolkit as E, selectedPullRequestSourceLayer as En, annotatePatch as Er, AdjudicationComment as Et, ReviewCandidate as F, FileDiffQuery as Fn, noReviewAdjudicationHostLayer as Ft, makeFileReviewerInstructions as G, MAX_PATCH_CHARS as Gn, ReviewRetirementHost as Gt, defaultFileReviewerPolicy as H, ListChangedFilesQuery as Hn, RetirableReview as Ht, ReviewCandidateId as I, FileDiffView as In, parseIssueAdjudication as It, MAX_MERGED_FINDINGS as J, PullRequestReviewer as Jn, hasReviewMetadataMarker as Jt, reviewCandidateSubjectKey as K, MAX_WALKTHROUGH_ENTRIES as Kn, ReviewRetirementReport as Kt, ReviewPassMisbehaved as L, FileSlice as Ln, parseThreadAdjudication as Lt, MAX_REVIEW_CHILDREN as M, ChangedFileSummary as Mn, deriveAdjudications as Mt, MAX_UNIT_CANDIDATES as N, ChangedFilesView as Nn, mergeAdjudications as Nt, FindingCandidate as O, toStoredFinding as On, hasReviewableContent as Or, ReviewAdjudicationFailure as Ot, REVIEW_UNIT_CONCURRENCY as P, CodeReview as Pn, noReviewAdjudicationHost as Pt, MAX_UNIT_FILES as Q, ReviewConcern as Qn, MAX_REVIEW_STATE_MARKER_CHARS as Qt, ReviewWorkPerspective as R, FileSliceQuery as Rn, renderAdjudicationContextLine as Rt, DiscoveredConcern as S, fullReviewExecutionContextLayer as Sn, ChangedFile as Sr, splitCarriedScope as St, FileReviewReport as T, selectReviewRange as Tn, MAX_REVIEW_CONTENT_CHARS as Tr, AdjudicableThread as Tt, fileReviewerInstructions as U, MAX_CONCERNS as Un, RetirableReviewComment as Ut, confirmedFindingForPublication as V, ListChangedFiles as Vn, INLINE_FINDING_TITLE_PATTERN as Vt, makeFileReviewerDefinition as W, MAX_FINDINGS as Wn, ReviewRetirementFailure as Wt, MAX_REVIEW_UNITS as X, ReadFile as Xn, AdjudicationDisposition as Xt, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as Y, REVIEW_TOOL_RESULT_MAX_BYTES as Yn, retireStaleReviews as Yt, MAX_UNIT_EVIDENCE_SHARDS as Z, ReadFileDiff as Zn, GitCommitSha as Zt, computeProfileFingerprint as _, buildProfileMission as _n, PullRequestMetadata as _r, ReviewInputCoverage as _t, PriorReviews as a, ReviewScopeMode as an, clampMaxFindings as ar, ReviewUnit as at, CandidateAssessment as b, fromStoredConcern as bn, ReviewInputViolation as br, fanOutInputCoverage as bt, fingerprintUnchanged as c, ReviewStateAuthenticator as cn, fileReviewEvidenceChunks as cr, UNIT_EVIDENCE_CHAR_BUDGET as ct, gitHubReviewAdjudicationHostLayer as d, StoredAdjudication as dn, readFileDiffHandler as dr, planReviewUnits as dt, MAX_STORED_UNREVIEWED_PASSES as en, ReviewMission as er, ReviewDiscoveryPerspective as et, gitHubReviewPublisherLayer as f, StoredReviewConcern as fn, readFileHandler as fr, rankAndDedupeConcerns as ft, computeChangesetFingerprint as g, adjudicationIdentity as gn, MAX_FILE_CHARS as gr, ReviewAssurance as gt, FINGERPRINT_MARKER_LENGTH as h, UnreviewedStage as hn, MAX_CHANGED_FILES as hr, FailedReviewPass as ht, PriorReviewLookupFailure as i, ReviewMode as in, WalkthroughEntry as ir, ReviewRiskCategory as it, MAX_FILE_REVIEW_TOOL_CALLS as j, webCryptoReviewStateAuthenticatorLayer as jn, renderReviewContent as jr, collectReviewAdjudications as jt, MAX_CHILD_CONCERNS as k, unavailableReviewStateAuthenticatorLayer as kn, isReviewableFile as kr, ReviewAdjudicationHost as kt, gitHubPriorReviewsLayer as l, ReviewStateMarker as ln, listChangedFilesHandler as lr, classifyReviewRisks as lt, parseGitHubSubmittedAt as m, StoredUnreviewedPass as mn, reviewInstructions as mr, reviewConcernKey as mt, GitHubApiFailure as n, ReviewExecutionContext as nn, ReviewToolkitLayer as nr, ReviewEvidenceShardId as nt, PublishedReview as o, ReviewState as on, defaultReviewPolicy as or, ReviewUnitId as ot, gitHubReviewRetirementHostLayer as p, StoredReviewFinding as pn, resolveGuidance as pr, rankAndDedupeFindings as pt, runFanOutReview as q, MAX_WALKTHROUGH_SUMMARY_CHARS as qn, decideReviewRetirement as qt, GitHubReviewTarget as r, ReviewHeadComparison as rn, ReviewVerdict as rr, ReviewPassId as rt, ReviewPublisher as s, ReviewStateAuthenticationFailure as sn, fileDiffView as sr, ReviewUnitPlan as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, MAX_STORED_UNREVIEWED_PATHS as tn, ReviewToolkit as tr, ReviewEvidenceShard as tt, gitHubPullRequestSourceLayer as u, ReviewStateMarkerTooLarge as un, makeReviewInstructions as ur, findingAnchorInUnitEvidence as ut, extractFingerprint as v, concernIdentity as vn, PullRequestSource as vr, assessFlatReview as vt, FileReviewEvidence as w, isLineageAncestor as wn, ChangedPath as wr, AUTHORIZED_ADJUDICATION_ASSOCIATIONS as wt, ConcernCandidate as x, fromStoredFinding as xn, normalizeRepoRelativePath as xr, flatAssurance as xt, renderFingerprintMarker as y, findingIdentity as yn, PullRequestSourceFailure as yr, boundedListReason as yt, ReviewWorkPhase as z, FindingCategory as zn, renderPriorFindingContextLine as zt };
3435
+ export { ReviewDiscoveryPass as $, ReadFileDiff as $n, MAX_STORED_ADJUDICATIONS as $t, MAX_CHILD_FINDINGS as A, toStoredFinding as An, hasReviewableContent as Ar, buildPriorReviewContext as At, assessmentSettlesSuggestionExactly as B, FileSliceQuery as Bn, threadFindingTarget as Bt, FileReviewBrief as C, fromStoredFinding as Cn, normalizeRepoRelativePath as Cr, anchorViolation as Ct, FileReviewer as D, selectReviewRange as Dn, MAX_REVIEW_CONTENT_CHARS as Dr, MAX_THREAD_ADJUDICATION_COMMANDS as Dt, FileReviewToolkit as E, isLineageAncestor as En, ChangedPath as Er, AdjudicationComment as Et, ReviewCandidate as F, ChangedFilesView as Fn, noReviewAdjudicationHostLayer as Ft, makeFileReviewerInstructions as G, MAX_CONCERNS as Gn, ReviewRetirementHost as Gt, defaultFileReviewerPolicy as H, FindingSeverity as Hn, RetirableReview as Ht, ReviewCandidateId as I, CodeReview as In, parseIssueAdjudication as It, MAX_MERGED_FINDINGS as J, MAX_WALKTHROUGH_ENTRIES as Jn, hasReviewMetadataMarker as Jt, reviewCandidateSubjectKey as K, MAX_FINDINGS as Kn, ReviewRetirementReport as Kt, ReviewPassMisbehaved as L, FileDiffQuery as Ln, parseThreadAdjudication as Lt, MAX_REVIEW_CHILDREN as M, validateReviewState as Mn, parsePatch as Mr, deriveAdjudications as Mt, MAX_UNIT_CANDIDATES as N, webCryptoReviewStateAuthenticatorLayer as Nn, renderReviewContent as Nr, mergeAdjudications as Nt, FindingCandidate as O, selectedPullRequestSourceLayer as On, annotatePatch as Or, ReviewAdjudicationFailure as Ot, REVIEW_UNIT_CONCURRENCY as P, ChangedFileSummary as Pn, noReviewAdjudicationHost as Pt, MAX_UNIT_FILES as Q, ReadFile as Qn, MAX_REVIEW_STATE_MARKER_CHARS as Qt, ReviewWorkPerspective as R, FileDiffView as Rn, renderAdjudicationContextLine as Rt, DiscoveredConcern as S, fromStoredConcern as Sn, ReviewInputViolation as Sr, splitCarriedScope as St, FileReviewReport as T, fullReviewSelection as Tn, ChangedFileStatus as Tr, AdjudicableThread as Tt, fileReviewerInstructions as U, ListChangedFiles as Un, RetirableReviewComment as Ut, confirmedFindingForPublication as V, FindingCategory as Vn, INLINE_FINDING_TITLE_PATTERN as Vt, makeFileReviewerDefinition as W, ListChangedFilesQuery as Wn, ReviewRetirementFailure as Wt, MAX_REVIEW_UNITS as X, PullRequestReviewer as Xn, AdjudicationDisposition as Xt, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as Y, MAX_WALKTHROUGH_SUMMARY_CHARS as Yn, retireStaleReviews as Yt, MAX_UNIT_EVIDENCE_SHARDS as Z, REVIEW_TOOL_RESULT_MAX_BYTES as Zn, GitCommitSha as Zt, computeProfileFingerprint as _, UnreviewedStage as _n, MAX_CHANGED_FILES as _r, ReviewInputCoverage as _t, PriorReviews as a, ReviewMode as an, ReviewVerdict as ar, ReviewUnit as at, CandidateAssessment as b, concernIdentity as bn, PullRequestSource as br, fanOutInputCoverage as bt, fingerprintUnchanged as c, ReviewStateAuthenticationFailure as cn, defaultReviewPolicy as cr, UNIT_EVIDENCE_CHAR_BUDGET as ct, gitHubReviewAdjudicationHostLayer as d, ReviewStateMarkerTooLarge as dn, listChangedFilesHandler as dr, planReviewUnits as dt, MAX_STORED_UNREVIEWED_PASSES as en, ReviewConcern as er, ReviewDiscoveryPerspective as et, gitHubReviewPublisherLayer as f, ReviewTreeComparison as fn, makeReviewInstructions as fr, rankAndDedupeConcerns as ft, computeChangesetFingerprint as g, StoredUnreviewedPass as gn, reviewInstructions as gr, ReviewAssurance as gt, FINGERPRINT_MARKER_LENGTH as h, StoredReviewFinding as hn, resolveGuidance as hr, FailedReviewPass as ht, PriorReviewLookupFailure as i, ReviewHeadComparison as in, ReviewToolkitLayer as ir, ReviewRiskCategory as it, MAX_FILE_REVIEW_TOOL_CALLS as j, unavailableReviewStateAuthenticatorLayer as jn, isReviewableFile as jr, collectReviewAdjudications as jt, MAX_CHILD_CONCERNS as k, toStoredConcern as kn, commentableLines as kr, ReviewAdjudicationHost as kt, gitHubPriorReviewsLayer as l, ReviewStateAuthenticator as ln, fileDiffView as lr, classifyReviewRisks as lt, parseGitHubSubmittedAt as m, StoredReviewConcern as mn, readFileHandler as mr, reviewConcernKey as mt, GitHubApiFailure as n, MAX_TREE_COMPARISON_PATHS as nn, ReviewMission as nr, ReviewEvidenceShardId as nt, PublishedReview as o, ReviewScopeMode as on, WalkthroughEntry as or, ReviewUnitId as ot, gitHubReviewRetirementHostLayer as p, StoredAdjudication as pn, readFileDiffHandler as pr, rankAndDedupeFindings as pt, runFanOutReview as q, MAX_PATCH_CHARS as qn, decideReviewRetirement as qt, GitHubReviewTarget as r, ReviewExecutionContext as rn, ReviewToolkit as rr, ReviewPassId as rt, ReviewPublisher as s, ReviewState as sn, clampMaxFindings as sr, ReviewUnitPlan as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, MAX_STORED_UNREVIEWED_PATHS as tn, ReviewFinding as tr, ReviewEvidenceShard as tt, gitHubPullRequestSourceLayer as u, ReviewStateMarker as un, fileReviewEvidenceChunks as ur, findingAnchorInUnitEvidence as ut, extractFingerprint as v, adjudicationIdentity as vn, MAX_FILE_CHARS as vr, assessFlatReview as vt, FileReviewEvidence as w, fullReviewExecutionContextLayer as wn, ChangedFile as wr, AUTHORIZED_ADJUDICATION_ASSOCIATIONS as wt, ConcernCandidate as x, findingIdentity as xn, PullRequestSourceFailure as xr, flatAssurance as xt, renderFingerprintMarker as y, buildProfileMission as yn, PullRequestMetadata as yr, boundedListReason as yt, ReviewWorkPhase as z, FileSlice as zn, renderPriorFindingContextLine as zt };
3263
3436
 
3264
- //# sourceMappingURL=github-NjgxGqwM.mjs.map
3437
+ //# sourceMappingURL=github-CCuLgyqb.mjs.map