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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { Context, DateTime, Effect, Encoding, Layer, Option, Redacted, Result, Schema } from "effect";
1
+ import { Context, Crypto, DateTime, Effect, Encoding, Layer, Option, Redacted, Result, Schema } from "effect";
2
2
  import { Agent, AgentPolicy, AgentRuntime, ToolExecutionClass, ToolResultBounds } from "effect-agent";
3
3
  import { Tool, Toolkit } from "effect/unstable/ai";
4
4
  import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
@@ -1569,7 +1569,11 @@ const reviewUnit = (binding, unit, passes, input) => Effect.gen(function* () {
1569
1569
  completedSpecialistPasses,
1570
1570
  requiredVerificationPasses,
1571
1571
  completedVerificationPasses,
1572
- unreviewedPaths: failedPasses.length > 0 ? unit.paths : []
1572
+ unreviewedPaths: failedPasses.length > 0 ? unit.paths : [],
1573
+ unreviewedPasses: failedPasses.map((pass) => ({
1574
+ stage: pass.stage,
1575
+ paths: unit.paths
1576
+ }))
1573
1577
  };
1574
1578
  });
1575
1579
  const countNoun = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
@@ -1584,6 +1588,90 @@ const composeSummary = (plan, assurance) => {
1584
1588
  parts.push("No configured pipeline can prove absence of defects; this describes settled work only.");
1585
1589
  return parts.join(" ").slice(0, 4e3);
1586
1590
  };
1591
+ const remapPlanUnitIds = (plan, offset) => {
1592
+ if (offset === 0) return plan;
1593
+ const units = plan.units.map((unit, index) => ReviewUnit.make({
1594
+ ...unit,
1595
+ unitId: `unit-${String(offset + index + 1).padStart(3, "0")}`
1596
+ }));
1597
+ const mappedIds = /* @__PURE__ */ new Map();
1598
+ for (const [index, unit] of plan.units.entries()) {
1599
+ const remapped = units[index];
1600
+ if (remapped !== void 0) mappedIds.set(unit.unitId, remapped.unitId);
1601
+ }
1602
+ return ReviewUnitPlan.make({
1603
+ ...plan,
1604
+ units,
1605
+ discoveryPasses: plan.discoveryPasses.map((pass) => {
1606
+ const unitId = mappedIds.get(pass.unitId) ?? pass.unitId;
1607
+ return ReviewDiscoveryPass.make({
1608
+ ...pass,
1609
+ unitId,
1610
+ passId: `${unitId}${pass.passId.slice(pass.unitId.length)}`
1611
+ });
1612
+ })
1613
+ });
1614
+ };
1615
+ const scheduleFanOutWork = (input) => {
1616
+ const retryPathSet = new Set(input.retry?.paths ?? []);
1617
+ const retryStages = new Set(input.retry?.stages ?? []);
1618
+ if (retryPathSet.size === 0) {
1619
+ const plan = planReviewUnits(input.files, { totalChangedFiles: input.totalChangedFiles });
1620
+ const passesByUnit = /* @__PURE__ */ new Map();
1621
+ for (const pass of plan.discoveryPasses) {
1622
+ const passes = passesByUnit.get(pass.unitId) ?? [];
1623
+ passes.push(pass);
1624
+ passesByUnit.set(pass.unitId, passes);
1625
+ }
1626
+ return {
1627
+ plan,
1628
+ passesByUnit,
1629
+ overflowRetryPaths: []
1630
+ };
1631
+ }
1632
+ const freshFiles = input.files.filter((file) => !retryPathSet.has(file.path));
1633
+ const retryFiles = input.files.filter((file) => retryPathSet.has(file.path));
1634
+ const freshPlan = planReviewUnits(freshFiles, { totalChangedFiles: input.totalChangedFiles });
1635
+ const retryPlan = remapPlanUnitIds(planReviewUnits(retryFiles, { totalChangedFiles: input.totalChangedFiles }), freshPlan.units.length);
1636
+ const acceptedFresh = freshPlan.units.slice(0, 8);
1637
+ const acceptedRetry = retryPlan.units.slice(0, Math.max(0, 8 - acceptedFresh.length));
1638
+ const overflowRetryPaths = retryPlan.units.slice(acceptedRetry.length).flatMap((unit) => [...unit.paths]);
1639
+ const retryPassFilter = (pass) => {
1640
+ const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
1641
+ return retryStages.has(stage);
1642
+ };
1643
+ const acceptedRetryIds = new Set(acceptedRetry.map((unit) => unit.unitId));
1644
+ let retryPasses = retryPlan.discoveryPasses.filter((pass) => acceptedRetryIds.has(pass.unitId) && retryPassFilter(pass));
1645
+ if (retryStages.has("verification") && !retryStages.has("discovery") && !retryStages.has("specialist")) retryPasses = retryPlan.discoveryPasses.filter((pass) => acceptedRetryIds.has(pass.unitId));
1646
+ const acceptedFreshIds = new Set(acceptedFresh.map((unit) => unit.unitId));
1647
+ const discoveryPasses = [...freshPlan.discoveryPasses.filter((pass) => acceptedFreshIds.has(pass.unitId)), ...retryPasses];
1648
+ const plan = ReviewUnitPlan.make({
1649
+ totalFiles: input.files.length,
1650
+ truncated: freshPlan.truncated || retryPlan.truncated,
1651
+ units: [...acceptedFresh, ...acceptedRetry],
1652
+ discoveryPasses,
1653
+ undiffablePaths: [.../* @__PURE__ */ new Set([...freshPlan.undiffablePaths, ...retryPlan.undiffablePaths])].sort(),
1654
+ partialEvidencePaths: [.../* @__PURE__ */ new Set([...freshPlan.partialEvidencePaths, ...retryPlan.partialEvidencePaths])].sort(),
1655
+ unassignedEvidenceShardCount: freshPlan.unassignedEvidenceShardCount + retryPlan.unassignedEvidenceShardCount,
1656
+ unassignedEvidenceShardIds: [...freshPlan.unassignedEvidenceShardIds, ...retryPlan.unassignedEvidenceShardIds].slice(0, 96),
1657
+ unassignedPaths: [.../* @__PURE__ */ new Set([
1658
+ ...freshPlan.unassignedPaths,
1659
+ ...retryPlan.unassignedPaths,
1660
+ ...overflowRetryPaths
1661
+ ])].sort()
1662
+ });
1663
+ const passesByUnit = /* @__PURE__ */ new Map();
1664
+ for (const pass of discoveryPasses) {
1665
+ const passes = passesByUnit.get(pass.unitId) ?? [];
1666
+ passes.push(pass);
1667
+ passesByUnit.set(pass.unitId, passes);
1668
+ }
1669
+ return {
1670
+ plan,
1671
+ passesByUnit,
1672
+ overflowRetryPaths
1673
+ };
1674
+ };
1587
1675
  /**
1588
1676
  * Run the complete host-scheduled fan-out pipeline over one selected
1589
1677
  * changeset snapshot: plan, independent discovery, exact verification, and a
@@ -1591,13 +1679,7 @@ const composeSummary = (plan, assurance) => {
1591
1679
  * only. The verdict is derived from confirmed severities, never model prose.
1592
1680
  */
1593
1681
  const runFanOutReview = (binding, input) => Effect.gen(function* () {
1594
- const plan = planReviewUnits(input.files, { totalChangedFiles: input.totalChangedFiles });
1595
- const passesByUnit = /* @__PURE__ */ new Map();
1596
- for (const pass of plan.discoveryPasses) {
1597
- const passes = passesByUnit.get(pass.unitId) ?? [];
1598
- passes.push(pass);
1599
- passesByUnit.set(pass.unitId, passes);
1600
- }
1682
+ const { plan, passesByUnit, overflowRetryPaths } = scheduleFanOutWork(input);
1601
1683
  const outcomes = yield* Effect.forEach(plan.units, (unit) => reviewUnit(binding, unit, passesByUnit.get(unit.unitId) ?? [], input), { concurrency: 4 });
1602
1684
  const failedPasses = outcomes.flatMap((outcome) => outcome.failedPasses);
1603
1685
  const unsettledCandidates = outcomes.reduce((total, outcome) => total + outcome.unsettledCandidates, 0);
@@ -1642,6 +1724,14 @@ const runFanOutReview = (binding, input) => Effect.gen(function* () {
1642
1724
  ...plan.partialEvidencePaths,
1643
1725
  ...plan.undiffablePaths
1644
1726
  ])].sort(),
1727
+ unreviewedPasses: [...outcomes.flatMap((outcome) => outcome.unreviewedPasses), ...overflowRetryPaths.length === 0 ? [] : (input.retry?.stages.length ? input.retry.stages : [
1728
+ "discovery",
1729
+ "specialist",
1730
+ "verification"
1731
+ ]).map((stage) => ({
1732
+ stage,
1733
+ paths: overflowRetryPaths
1734
+ }))],
1645
1735
  turns: outcomes.reduce((total, outcome) => total + outcome.turns, 0)
1646
1736
  };
1647
1737
  });
@@ -1660,10 +1750,10 @@ const extractFingerprint = (body) => {
1660
1750
  for (const match of body.matchAll(MARKER_PATTERN)) last = match[1];
1661
1751
  return last;
1662
1752
  };
1663
- /** WebCrypto SHA-256; unavailable crypto is a defect, not an expected failure. */
1664
- const sha256Hex = (text) => Effect.promise(async () => {
1665
- const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
1666
- return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1753
+ /** SHA-256 via `Crypto.Crypto`; unavailable crypto is a defect, not an expected failure. */
1754
+ const sha256Hex = Effect.fn("sha256Hex")(function* (text) {
1755
+ const digest = yield* (yield* Crypto.Crypto).digest("SHA-256", new TextEncoder().encode(text)).pipe(Effect.orDie);
1756
+ return Encoding.encodeHex(digest);
1667
1757
  });
1668
1758
  const FIELD = "\0";
1669
1759
  const RECORD = "";
@@ -1687,6 +1777,8 @@ const canonicalChangeset = (files) => files.map((file) => `${file.path}${FIELD}$
1687
1777
  * not carry.
1688
1778
  */
1689
1779
  const computeChangesetFingerprint = (files, signature) => sha256Hex(`${canonicalChangeset(files)}${SECTION}${signature}`);
1780
+ /** Profile fingerprints are SHA-256 over configuration-only signatures. */
1781
+ const computeProfileFingerprint = (signature) => sha256Hex(signature);
1690
1782
  //#endregion
1691
1783
  //#region src/internal/review-state.ts
1692
1784
  const ReviewMode = Schema.Literals(["incremental", "final"]);
@@ -1711,6 +1803,19 @@ var StoredReviewConcern = class extends Schema.Class("@effect-agent/pr-review/St
1711
1803
  }) {};
1712
1804
  /** The carried-scope bound; a run that cannot fit its leftovers publishes no state. */
1713
1805
  const MAX_STORED_UNREVIEWED_PATHS = 100;
1806
+ /** Failed-pass records stored beside the leftover paths; one per unit stage. */
1807
+ const MAX_STORED_UNREVIEWED_PASSES = 24;
1808
+ /** Stages a leftover path may need retried without a second general discovery. */
1809
+ const UnreviewedStage = Schema.Literals([
1810
+ "discovery",
1811
+ "specialist",
1812
+ "verification"
1813
+ ]);
1814
+ /** One failed fan-out pass whose paths should be retried, not rediscovered. */
1815
+ var StoredUnreviewedPass = class extends Schema.Class("@effect-agent/pr-review/StoredUnreviewedPass")({
1816
+ stage: UnreviewedStage,
1817
+ paths: Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(12))
1818
+ }) {};
1714
1819
  /**
1715
1820
  * Versioned state embedded after EVERY completed run that can be signed. The
1716
1821
  * head plus full-scope fingerprint forms an incremental baseline; an absent
@@ -1741,6 +1846,12 @@ var ReviewState = class extends Schema.Class("@effect-agent/pr-review/ReviewStat
1741
1846
  /** Retryable review gaps carried into the next incremental run's scope. */
1742
1847
  unreviewedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(100)),
1743
1848
  /**
1849
+ * Which failed pass produced those leftovers. Absent on state-v2 markers
1850
+ * written before this field existed; those leftovers still re-enter scope
1851
+ * but cannot skip rediscovery. Present (including empty) on new markers.
1852
+ */
1853
+ unreviewedPasses: Schema.optionalKey(Schema.Array(StoredUnreviewedPass).check(Schema.isMaxLength(24))),
1854
+ /**
1744
1855
  * True only when the producing run had complete input coverage, no
1745
1856
  * unsettled pass, and nothing carried. Skip-unchanged authority: an
1746
1857
  * unchanged patch may skip re-review only over a settled state.
@@ -1883,11 +1994,15 @@ const fullSelection = (input) => ({
1883
1994
  reason: input.reason,
1884
1995
  files: input.files,
1885
1996
  affectedPaths: input.files.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]),
1997
+ retryPaths: [],
1998
+ retryStages: [],
1886
1999
  totalFiles: input.totalFiles,
1887
2000
  baselineSha: void 0,
1888
2001
  priorState: void 0,
1889
2002
  profileFingerprint: input.profileFingerprint
1890
2003
  });
2004
+ /** Three-dot lineage from the reviewed head to the current head is usable. */
2005
+ const isLineageAncestor = (comparison, priorState, currentHeadSha) => comparison.baseSha === priorState.reviewedHeadSha && comparison.headSha === currentHeadSha && comparison.mergeBaseSha === priorState.reviewedHeadSha && !comparison.truncated && (comparison.status === "ahead" || comparison.status === "identical");
1891
2006
  /**
1892
2007
  * Validate that persisted state belongs to this exact PR/base lineage and the
1893
2008
  * same review profile. A mismatch is a full-review reason, never an error that
@@ -1900,6 +2015,52 @@ const validateReviewState = (state, current, profileFingerprint) => {
1900
2015
  if (state.headRef !== current.headRef) return "the pull request head ref changed";
1901
2016
  if (state.profileFingerprint !== profileFingerprint) return "the reviewer profile or model configuration changed";
1902
2017
  };
2018
+ const filePaths = (file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath];
2019
+ const incrementalFromDelta = (input) => {
2020
+ const currentPaths = new Set(input.fullFiles.flatMap(filePaths));
2021
+ const affectedPaths = /* @__PURE__ */ new Set([...input.deltaFiles.flatMap(filePaths), ...input.extraAffectedPaths ?? []]);
2022
+ const selectedByPath = /* @__PURE__ */ new Map();
2023
+ for (const file of input.deltaFiles) if (currentPaths.has(file.path) || file.previousPath !== void 0 && currentPaths.has(file.previousPath)) selectedByPath.set(file.path, file);
2024
+ const carriedPaths = input.priorState.unreviewedPaths.filter((path) => currentPaths.has(path));
2025
+ const surgical = input.priorState.unreviewedPasses !== void 0;
2026
+ const retryOnly = /* @__PURE__ */ new Set();
2027
+ const retryStages = /* @__PURE__ */ new Set();
2028
+ for (const path of carriedPaths) {
2029
+ if (affectedPaths.has(path)) continue;
2030
+ retryOnly.add(path);
2031
+ if (surgical) {
2032
+ for (const pass of input.priorState.unreviewedPasses ?? []) if (pass.paths.includes(path)) retryStages.add(pass.stage);
2033
+ } else affectedPaths.add(path);
2034
+ }
2035
+ if (surgical && retryStages.has("verification") && !retryStages.has("discovery") && !retryStages.has("specialist")) {
2036
+ retryStages.add("discovery");
2037
+ retryStages.add("specialist");
2038
+ }
2039
+ if (surgical && retryOnly.size > 0 && retryStages.size === 0) {
2040
+ for (const path of retryOnly) affectedPaths.add(path);
2041
+ retryStages.add("discovery");
2042
+ retryStages.add("specialist");
2043
+ retryStages.add("verification");
2044
+ }
2045
+ if (carriedPaths.length > 0 || (input.extraAffectedPaths?.length ?? 0) > 0) {
2046
+ 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);
2047
+ }
2048
+ const selectedFiles = [...selectedByPath.values()].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
2049
+ const leftoverCount = [...retryOnly].filter((path) => !affectedPaths.has(path)).length;
2050
+ const carriedReason = leftoverCount > 0 && surgical ? `; retrying ${leftoverCount} unchanged leftover path(s) without rediscovery` : carriedPaths.length > 0 ? `; retrying ${carriedPaths.length} carried unreviewed path(s)` : "";
2051
+ return {
2052
+ mode: "incremental",
2053
+ reason: `${input.reason}${carriedReason}`,
2054
+ files: selectedFiles,
2055
+ affectedPaths: [...affectedPaths].sort(),
2056
+ retryPaths: [...retryOnly].filter((path) => !affectedPaths.has(path)).sort(),
2057
+ retryStages: [...retryStages].sort(),
2058
+ totalFiles: selectedFiles.length,
2059
+ baselineSha: input.priorState.reviewedHeadSha,
2060
+ priorState: input.priorState,
2061
+ profileFingerprint: input.profileFingerprint
2062
+ };
2063
+ };
1903
2064
  /** Pure, deterministic range selection with conservative full-review fallbacks. */
1904
2065
  const selectReviewRange = (input) => {
1905
2066
  const full = (reason) => fullSelection({
@@ -1914,42 +2075,38 @@ const selectReviewRange = (input) => {
1914
2075
  const invalid = validateReviewState(input.priorState, input.current, input.profileFingerprint);
1915
2076
  if (invalid !== void 0) return full(invalid);
1916
2077
  const comparison = input.comparison;
1917
- if (comparison === void 0) return full("the incremental head comparison was unavailable");
1918
- if (comparison.baseSha !== input.priorState.reviewedHeadSha || comparison.headSha !== input.current.headSha || comparison.mergeBaseSha !== input.priorState.reviewedHeadSha || comparison.status !== "ahead" && comparison.status !== "identical") return full("the prior reviewed head is not an ancestor of the current head");
1919
- if (comparison.truncated) return full("the incremental comparison exceeded GitHub's file bound");
1920
- const affectedPaths = new Set(comparison.files.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]));
1921
- let baseReason = "";
1922
- if (input.priorState.baseSha !== input.current.baseSha) {
1923
- const baseComparison = input.baseComparison;
1924
- if (baseComparison === void 0) return full("the pull request base changed and its lineage comparison was unavailable");
1925
- if (baseComparison.baseSha !== input.priorState.baseSha || baseComparison.headSha !== input.current.baseSha || baseComparison.mergeBaseSha !== input.priorState.baseSha || baseComparison.status !== "ahead" && baseComparison.status !== "identical" || baseComparison.truncated) return full("the pull request base changed materially or exceeded the comparison bound");
1926
- for (const file of baseComparison.files) {
1927
- affectedPaths.add(file.path);
1928
- if (file.previousPath !== void 0) affectedPaths.add(file.previousPath);
2078
+ if (comparison !== void 0 && isLineageAncestor(comparison, input.priorState, input.current.headSha)) {
2079
+ const extraAffected = [];
2080
+ let baseReason = "";
2081
+ if (input.priorState.baseSha !== input.current.baseSha) {
2082
+ const baseComparison = input.baseComparison;
2083
+ if (baseComparison === void 0) return full("the pull request base changed and its lineage comparison was unavailable");
2084
+ if (baseComparison.baseSha !== input.priorState.baseSha || baseComparison.headSha !== input.current.baseSha || baseComparison.mergeBaseSha !== input.priorState.baseSha || baseComparison.status !== "ahead" && baseComparison.status !== "identical" || baseComparison.truncated) return full("the pull request base changed materially or exceeded the comparison bound");
2085
+ for (const file of baseComparison.files) extraAffected.push(...filePaths(file));
2086
+ baseReason = `; base advanced from ${input.priorState.baseSha.slice(0, 7)} and overlapping PR paths were included`;
1929
2087
  }
1930
- baseReason = `; base advanced from ${input.priorState.baseSha.slice(0, 7)} and overlapping PR paths were included`;
1931
- }
1932
- const currentPaths = new Set(input.fullFiles.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]));
1933
- const selectedByPath = /* @__PURE__ */ new Map();
1934
- for (const file of comparison.files) if (currentPaths.has(file.path) || file.previousPath !== void 0 && currentPaths.has(file.previousPath)) selectedByPath.set(file.path, file);
1935
- const carriedPaths = new Set(input.priorState.unreviewedPaths.filter((path) => currentPaths.has(path)));
1936
- for (const path of carriedPaths) affectedPaths.add(path);
1937
- const rescuePaths = input.priorState.baseSha !== input.current.baseSha;
1938
- if (rescuePaths || carriedPaths.size > 0) {
1939
- for (const file of input.fullFiles) if (rescuePaths && (affectedPaths.has(file.path) || file.previousPath !== void 0 && affectedPaths.has(file.previousPath)) || carriedPaths.has(file.path) || file.previousPath !== void 0 && carriedPaths.has(file.previousPath)) selectedByPath.set(file.path, file);
2088
+ return incrementalFromDelta({
2089
+ current: input.current,
2090
+ fullFiles: input.fullFiles,
2091
+ profileFingerprint: input.profileFingerprint,
2092
+ priorState: input.priorState,
2093
+ deltaFiles: comparison.files,
2094
+ extraAffectedPaths: extraAffected,
2095
+ reason: `changes since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}`
2096
+ });
1940
2097
  }
1941
- const selectedFiles = [...selectedByPath.values()].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
1942
- const carriedReason = carriedPaths.size === 0 ? "" : `; retrying ${carriedPaths.size} carried unreviewed path(s)`;
1943
- return {
1944
- mode: "incremental",
1945
- reason: `changes since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}${carriedReason}`,
1946
- files: selectedFiles,
1947
- affectedPaths: [...affectedPaths].sort(),
1948
- totalFiles: selectedFiles.length,
1949
- baselineSha: input.priorState.reviewedHeadSha,
2098
+ const contentComparison = input.contentComparison;
2099
+ if (contentComparison !== void 0 && !contentComparison.truncated) return incrementalFromDelta({
2100
+ current: input.current,
2101
+ fullFiles: input.fullFiles,
2102
+ profileFingerprint: input.profileFingerprint,
1950
2103
  priorState: input.priorState,
1951
- profileFingerprint: input.profileFingerprint
1952
- };
2104
+ deltaFiles: contentComparison.files,
2105
+ reason: `rewritten history; contents changed since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}`
2106
+ });
2107
+ if (comparison === void 0) return full("the incremental head comparison was unavailable");
2108
+ if (comparison.truncated) return full("the incremental comparison exceeded GitHub's file bound");
2109
+ return full("the prior reviewed head is not an ancestor of the current head");
1953
2110
  };
1954
2111
  /** Per-run context consumed by orchestration and publication, not by the model. */
1955
2112
  var ReviewExecutionContext = class extends Context.Service()("@effect-agent/pr-review/ReviewExecutionContext") {};
@@ -1983,11 +2140,6 @@ const selectedPullRequestSourceLayer = (selection) => Layer.effect(PullRequestSo
1983
2140
  }))
1984
2141
  });
1985
2142
  }));
1986
- /** Profile fingerprints are SHA-256 over configuration-only signatures. */
1987
- const computeProfileFingerprint = (signature) => Effect.promise(async () => {
1988
- const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(signature));
1989
- return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1990
- });
1991
2143
  /** Build the full-surface mission used only to resolve profile guidance. */
1992
2144
  const buildProfileMission = (metadata, files) => ReviewMission.make({
1993
2145
  repository: metadata.repository,
@@ -2250,8 +2402,8 @@ const GitHubReviewCommentWire = Schema.Struct({
2250
2402
  node_id: Schema.String,
2251
2403
  path: Schema.String,
2252
2404
  body: Schema.String,
2253
- line: Schema.NullOr(Schema.Int),
2254
- original_line: Schema.NullOr(Schema.Int),
2405
+ line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
2406
+ original_line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
2255
2407
  start_line: Schema.optionalKey(Schema.NullOr(Schema.Int)),
2256
2408
  original_start_line: Schema.optionalKey(Schema.NullOr(Schema.Int))
2257
2409
  });
@@ -2492,8 +2644,9 @@ const gitHubReviewRetirementHostLayer = Layer.effect(ReviewRetirementHost)(Effec
2492
2644
  url: `${prefix}/reviews/${reviewId}/comments`,
2493
2645
  decode: decodeRetirement(GitHubReviewCommentsPageWire, "listReviewCommentsForRetirement")
2494
2646
  }).pipe(Effect.map((comments) => comments.map((comment) => {
2495
- const endLine = comment.line ?? comment.original_line;
2496
- const startLine = comment.start_line ?? comment.original_start_line ?? endLine;
2647
+ const positiveLine = (value) => value !== void 0 && value !== null && value > 0 ? value : null;
2648
+ const endLine = positiveLine(comment.line ?? comment.original_line);
2649
+ const startLine = positiveLine(comment.start_line ?? comment.original_start_line) ?? endLine;
2497
2650
  return RetirableReviewComment.make({
2498
2651
  nodeId: comment.node_id,
2499
2652
  path: comment.path,
@@ -2583,8 +2736,8 @@ const gitHubPriorReviewsLayer = Layer.effect(PriorReviews)(Effect.gen(function*
2583
2736
  latestState
2584
2737
  };
2585
2738
  }).pipe(Effect.provideService(HttpClient.HttpClient, client));
2586
- const compareHeads = (baseSha, headSha) => Effect.gen(function* () {
2587
- 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))));
2739
+ const compareCommits = (baseSha, headSha, separator) => Effect.gen(function* () {
2740
+ 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))));
2588
2741
  const files = wire.files.map(toChangedFile);
2589
2742
  return ReviewHeadComparison.make({
2590
2743
  status: wire.status,
@@ -2595,13 +2748,22 @@ const gitHubPriorReviewsLayer = Layer.effect(PriorReviews)(Effect.gen(function*
2595
2748
  truncated: files.length >= 300
2596
2749
  });
2597
2750
  }).pipe(Effect.provideService(HttpClient.HttpClient, client));
2751
+ const compareTrees = (baseSha, headSha) => compareCommits(baseSha, headSha, "..").pipe(Effect.map((comparison) => ReviewHeadComparison.make({
2752
+ status: comparison.status === "identical" ? "identical" : "ahead",
2753
+ baseSha,
2754
+ headSha,
2755
+ mergeBaseSha: baseSha,
2756
+ files: comparison.files,
2757
+ truncated: comparison.truncated
2758
+ })));
2598
2759
  return PriorReviews.of({
2599
2760
  latestFingerprint: readMarkers(Option.none()).pipe(Effect.map((markers) => markers.latestFingerprint)),
2600
2761
  latestState: Effect.gen(function* () {
2601
2762
  const authenticator = yield* ReviewStateAuthenticator;
2602
2763
  return yield* readMarkers(Option.some(authenticator)).pipe(Effect.map((markers) => markers.latestState));
2603
2764
  }),
2604
- compareHeads
2765
+ compareHeads: (baseSha, headSha) => compareCommits(baseSha, headSha, "..."),
2766
+ compareTrees
2605
2767
  });
2606
2768
  }));
2607
2769
  /**
@@ -2614,6 +2776,6 @@ const fingerprintUnchanged = (current) => Effect.gen(function* () {
2614
2776
  return Option.isSome(latest) && latest.value === current;
2615
2777
  });
2616
2778
  //#endregion
2617
- export { DiscoveredConcern as $, commentableLines as $n, boundedListReason as $t, ReviewStateAuthenticationFailure as A, clampMaxFindings as An, MAX_UNIT_FILES as At, selectReviewRange as B, MAX_CHANGED_FILES as Bn, UNIT_CHANGED_LINE_BUDGET as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, ReviewConcern as Cn, reviewCandidateSubjectKey as Ct, ReviewMode as D, ReviewToolkitLayer as Dn, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as Dt, ReviewHeadComparison as E, ReviewToolkit as En, MAX_MERGED_FINDINGS as Et, StoredReviewFinding as F, makeReviewInstructions as Fn, ReviewPassId as Ft, validateReviewState as G, ReviewInputViolation as Gn, rankAndDedupeConcerns as Gt, toStoredConcern as H, PullRequestMetadata as Hn, classifyReviewRisks as Ht, buildProfileMission as I, readFileDiffHandler as In, ReviewRiskCategory as It, computeChangesetFingerprint as J, ChangedFile as Jn, FailedReviewUnit as Jt, webCryptoReviewStateAuthenticatorLayer as K, normalizeRepoRelativePath as Kn, rankAndDedupeFindings as Kt, computeProfileFingerprint as L, readFileHandler as Ln, ReviewUnit as Lt, ReviewStateMarker as M, fileDiffView as Mn, ReviewDiscoveryPerspective as Mt, ReviewStateMarkerTooLarge as N, fileReviewEvidenceChunks as Nn, ReviewEvidenceShard as Nt, ReviewScopeMode as O, ReviewVerdict as On, MAX_REVIEW_UNITS as Ot, StoredReviewConcern as P, listChangedFilesHandler as Pn, ReviewEvidenceShardId as Pt, ConcernCandidate as Q, annotatePatch as Qn, assessFlatReview as Qt, fromStoredConcern as R, resolveGuidance as Rn, ReviewUnitId as Rt, GitCommitSha as S, ReadFileDiff as Sn, makeFileReviewerInstructions as St, ReviewExecutionContext as T, ReviewMission as Tn, MAX_FILE_EVIDENCE_CHARS as Tt, toStoredFinding as U, PullRequestSource as Un, findingAnchorInUnitEvidence as Ut, selectedPullRequestSourceLayer as V, MAX_FILE_CHARS as Vn, UNIT_EVIDENCE_CHAR_BUDGET as Vt, unavailableReviewStateAuthenticatorLayer as W, PullRequestSourceFailure as Wn, planReviewUnits as Wt, renderFingerprintMarker as X, ChangedPath as Xn, ReviewCoverage as Xt, extractFingerprint as Y, ChangedFileStatus as Yn, ReviewAssurance as Yt, CandidateAssessment as Z, MAX_REVIEW_CONTENT_CHARS as Zn, ReviewInputCoverage as Zt, ReviewRetirementHost as _, MAX_WALKTHROUGH_ENTRIES as _n, assessmentSettlesSuggestionExactly as _t, PriorReviews as a, CodeReview as an, FindingCandidate as at, hasReviewMetadataMarker as b, REVIEW_TOOL_RESULT_MAX_BYTES as bn, fileReviewerInstructions as bt, fingerprintUnchanged as c, FileSlice as cn, MAX_FILE_REVIEW_TOOL_CALLS as ct, gitHubReviewPublisherLayer as d, FindingSeverity as dn, REVIEW_UNIT_CONCURRENCY as dt, compatibilityCoverage as en, hasReviewableContent as er, FileReviewBrief as et, gitHubReviewRetirementHostLayer as f, ListChangedFiles as fn, ReviewCandidate as ft, ReviewRetirementFailure as g, MAX_PATCH_CHARS as gn, ReviewWorkPhase as gt, RetirableReviewComment as h, MAX_FINDINGS as hn, ReviewWorkPerspective as ht, PriorReviewLookupFailure as i, ChangedFilesView as in, FileReviewer as it, ReviewStateAuthenticator as j, defaultReviewPolicy as jn, ReviewDiscoveryPass as jt, ReviewState as k, WalkthroughEntry as kn, MAX_UNIT_EVIDENCE_SHARDS as kt, gitHubPriorReviewsLayer as l, FileSliceQuery as ln, MAX_REVIEW_CHILDREN as lt, RetirableReview as m, MAX_CONCERNS as mn, ReviewPassMisbehaved as mt, GitHubApiFailure as n, flatAssurance as nn, parsePatch as nr, FileReviewReport as nt, PublishedReview as o, FileDiffQuery as on, MAX_CHILD_CONCERNS as ot, parseGitHubSubmittedAt as p, ListChangedFilesQuery as pn, ReviewCandidateId as pt, FINGERPRINT_MARKER_LENGTH as q, anchorViolation as qn, FailedReviewPass as qt, GitHubReviewTarget as r, ChangedFileSummary as rn, renderReviewContent as rr, FileReviewToolkit as rt, ReviewPublisher as s, FileDiffView as sn, MAX_CHILD_FINDINGS as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, fanOutInputCoverage as tn, isReviewableFile as tr, FileReviewEvidence as tt, gitHubPullRequestSourceLayer as u, FindingCategory as un, MAX_UNIT_CANDIDATES as ut, ReviewRetirementReport as v, MAX_WALKTHROUGH_SUMMARY_CHARS as vn, confirmedFindingForPublication as vt, MAX_STORED_UNREVIEWED_PATHS as w, ReviewFinding as wn, runFanOutReview as wt, retireStaleReviews as x, ReadFile as xn, makeFileReviewerDefinition as xt, decideReviewRetirement as y, PullRequestReviewer as yn, defaultFileReviewerPolicy as yt, fromStoredFinding as z, reviewInstructions as zn, ReviewUnitPlan as zt };
2779
+ export { extractFingerprint as $, ChangedFileStatus as $n, ReviewAssurance as $t, ReviewState as A, ReviewToolkit as An, MAX_MERGED_FINDINGS as At, fromStoredConcern as B, readFileDiffHandler as Bn, ReviewRiskCategory as Bt, MAX_REVIEW_STATE_MARKER_CHARS as C, PullRequestReviewer as Cn, defaultFileReviewerPolicy as Ct, ReviewHeadComparison as D, ReviewConcern as Dn, reviewCandidateSubjectKey as Dt, ReviewExecutionContext as E, ReadFileDiff as En, makeFileReviewerInstructions as Et, StoredReviewConcern as F, defaultReviewPolicy as Fn, ReviewDiscoveryPass as Ft, toStoredConcern as G, MAX_FILE_CHARS as Gn, UNIT_EVIDENCE_CHAR_BUDGET as Gt, isLineageAncestor as H, resolveGuidance as Hn, ReviewUnitId as Ht, StoredReviewFinding as I, fileDiffView as In, ReviewDiscoveryPerspective as It, validateReviewState as J, PullRequestSourceFailure as Jn, planReviewUnits as Jt, toStoredFinding as K, PullRequestMetadata as Kn, classifyReviewRisks as Kt, StoredUnreviewedPass as L, fileReviewEvidenceChunks as Ln, ReviewEvidenceShard as Lt, ReviewStateAuthenticator as M, ReviewVerdict as Mn, MAX_REVIEW_UNITS as Mt, ReviewStateMarker as N, WalkthroughEntry as Nn, MAX_UNIT_EVIDENCE_SHARDS as Nt, ReviewMode as O, ReviewFinding as On, runFanOutReview as Ot, ReviewStateMarkerTooLarge as P, clampMaxFindings as Pn, MAX_UNIT_FILES as Pt, computeProfileFingerprint as Q, ChangedFile as Qn, FailedReviewUnit as Qt, UnreviewedStage as R, listChangedFilesHandler as Rn, ReviewEvidenceShardId as Rt, GitCommitSha as S, MAX_WALKTHROUGH_SUMMARY_CHARS as Sn, confirmedFindingForPublication as St, MAX_STORED_UNREVIEWED_PATHS as T, ReadFile as Tn, makeFileReviewerDefinition as Tt, selectReviewRange as U, reviewInstructions as Un, ReviewUnitPlan as Ut, fromStoredFinding as V, readFileHandler as Vn, ReviewUnit as Vt, selectedPullRequestSourceLayer as W, MAX_CHANGED_FILES as Wn, UNIT_CHANGED_LINE_BUDGET as Wt, FINGERPRINT_MARKER_LENGTH as X, normalizeRepoRelativePath as Xn, rankAndDedupeFindings as Xt, webCryptoReviewStateAuthenticatorLayer as Y, ReviewInputViolation as Yn, rankAndDedupeConcerns as Yt, computeChangesetFingerprint as Z, anchorViolation as Zn, FailedReviewPass as Zt, ReviewRetirementHost as _, ListChangedFilesQuery as _n, ReviewCandidateId as _t, PriorReviews as a, fanOutInputCoverage as an, isReviewableFile as ar, FileReviewEvidence as at, hasReviewMetadataMarker as b, MAX_PATCH_CHARS as bn, ReviewWorkPhase as bt, fingerprintUnchanged as c, ChangedFilesView as cn, FileReviewer as ct, gitHubReviewPublisherLayer as d, FileDiffView as dn, MAX_CHILD_FINDINGS as dt, ReviewCoverage as en, ChangedPath as er, renderFingerprintMarker as et, gitHubReviewRetirementHostLayer as f, FileSlice as fn, MAX_FILE_REVIEW_TOOL_CALLS as ft, ReviewRetirementFailure as g, ListChangedFiles as gn, ReviewCandidate as gt, RetirableReviewComment as h, FindingSeverity as hn, REVIEW_UNIT_CONCURRENCY as ht, PriorReviewLookupFailure as i, compatibilityCoverage as in, hasReviewableContent as ir, FileReviewBrief as it, ReviewStateAuthenticationFailure as j, ReviewToolkitLayer as jn, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS as jt, ReviewScopeMode as k, ReviewMission as kn, MAX_FILE_EVIDENCE_CHARS as kt, gitHubPriorReviewsLayer as l, CodeReview as ln, FindingCandidate as lt, RetirableReview as m, FindingCategory as mn, MAX_UNIT_CANDIDATES as mt, GitHubApiFailure as n, assessFlatReview as nn, annotatePatch as nr, ConcernCandidate as nt, PublishedReview as o, flatAssurance as on, parsePatch as or, FileReviewReport as ot, parseGitHubSubmittedAt as p, FileSliceQuery as pn, MAX_REVIEW_CHILDREN as pt, unavailableReviewStateAuthenticatorLayer as q, PullRequestSource as qn, findingAnchorInUnitEvidence as qt, GitHubReviewTarget as r, boundedListReason as rn, commentableLines as rr, DiscoveredConcern as rt, ReviewPublisher as s, ChangedFileSummary as sn, renderReviewContent as sr, FileReviewToolkit as st, DEFAULT_GITHUB_REVIEW_AUTHOR_LOGIN as t, ReviewInputCoverage as tn, MAX_REVIEW_CONTENT_CHARS as tr, CandidateAssessment as tt, gitHubPullRequestSourceLayer as u, FileDiffQuery as un, MAX_CHILD_CONCERNS as ut, ReviewRetirementReport as v, MAX_CONCERNS as vn, ReviewPassMisbehaved as vt, MAX_STORED_UNREVIEWED_PASSES as w, REVIEW_TOOL_RESULT_MAX_BYTES as wn, fileReviewerInstructions as wt, retireStaleReviews as x, MAX_WALKTHROUGH_ENTRIES as xn, assessmentSettlesSuggestionExactly as xt, decideReviewRetirement as y, MAX_FINDINGS as yn, ReviewWorkPerspective as yt, buildProfileMission as z, makeReviewInstructions as zn, ReviewPassId as zt };
2618
2780
 
2619
- //# sourceMappingURL=github-BbwYzNrC.mjs.map
2781
+ //# sourceMappingURL=github-C6jrBLA2.mjs.map