ace-git 0.25.0 → 0.26.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '089e377ec56099fb872544bfc754f6f95f00ecaf157715ecad93e726e9c8960f'
4
- data.tar.gz: 35bb8b556655c0ef1804fb2fe26039dd182115e558127254c49e17da9ce3f1ba
3
+ metadata.gz: 0b306e7eeb68db0b211dead9498aabdeba48ca866b13a3710a886193232d310a
4
+ data.tar.gz: db3448da7471947520a0aa68f70c56283f3539412456b221809ab7cd0349f976
5
5
  SHA512:
6
- metadata.gz: c156b7e8b123f15f9d305d27933d68de72d640e16a803a9d7b5ce2cce12c1c7cf537bb7e6ae31e7c8ca044999869160ccf835281fdcc51e3bee76589cd43363f
7
- data.tar.gz: 4eaeac69d71391c491718c68e68e1599a0a3e1991eae4a25d3e4e2a922959ef13e940c7d863eae01478a78ae9f81940a016fcac46da27ee7bbdf427f64527a03
6
+ metadata.gz: 0af17d14185c802e2a72f1f93c99054ea31d77ff86e3e2fd62adc2f30a99d3384dbc1f1f64de41ede47175f19ba334843ed553e775770839c41b3fc6a8232372
7
+ data.tar.gz: 6a5229b535fe9a4c586d1fd2e332daf90b57b0f91a899727cc677e3750aff38d55eb58f010ba1363e3956227b5f0f51879128f1bf18effac22f47236afbb53f1
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.26.0] - 2026-10-02
11
+
12
+ ### Added
13
+ - Normalized PR comment and review evidence, comment mutation receipts, and guarded review snapshots with exact repository and head identity.
14
+ - Recheck PR heads around collection and before review comments or thread mutations; classify unsupported capabilities and uncertain post outcomes.
15
+
16
+
10
17
  ## [0.25.0] - 2026-09-28
11
18
 
12
19
  ### Added
@@ -45,6 +45,134 @@ module Ace
45
45
  provider_for(server).pull_request(number: reference.number)
46
46
  end
47
47
 
48
+ # Exact-head PR metadata without the diff/comment/check inventory.
49
+ # Delta resolution needs identity and base provenance only; pulling
50
+ # the full diff there would transfer it just to discard it.
51
+ def review_metadata_snapshot(identifier)
52
+ reference = parse_identifier(identifier)
53
+ server = resolve_server_for(reference)
54
+ provider = provider_for(server)
55
+ before = provider.pull_request(number: reference.number)
56
+ head = required_head!(before)
57
+ details = provider.pull_request_review_details(number: reference.number)
58
+ body_before = provider.pull_request_body(number: reference.number)
59
+ # Hydration is a remote read: build the metadata from the read that
60
+ # follows it, and re-read the body after — the task spec text lives
61
+ # in the body, so an edit between reads invalidates the collection.
62
+ after = provider.pull_request(number: reference.number)
63
+ body_after = provider.pull_request_body(number: reference.number)
64
+ if body_before != body_after
65
+ raise ProviderExpectedHeadConflictError,
66
+ "PR body changed while collecting review metadata; retry on the current head"
67
+ end
68
+ after = after.with(body: body_after) unless body_after.nil?
69
+ # Body hydration and identity must agree with the base provenance
70
+ # read before collection; a moved base invalidates the metadata.
71
+ latest_details = provider.pull_request_review_details(number: reference.number)
72
+ if after.head_sha != head || after.head_ref != before.head_ref ||
73
+ after.base_ref != before.base_ref || latest_details.base_sha != details.base_sha
74
+ raise ProviderExpectedHeadConflictError,
75
+ "PR head/base changed while collecting review metadata; retry on the current head"
76
+ end
77
+ ProviderReviewSnapshot.new(
78
+ provider: server.provider, pull_request: after,
79
+ base_sha: details.base_sha, files: details.files,
80
+ diff: nil, review_evidence: ProviderReviewEvidence.new(
81
+ server_name: server.name, repository_url: server.url,
82
+ pr_number: before.number, head_sha: head, comments: [], reviews: []
83
+ ),
84
+ checks: []
85
+ )
86
+ end
87
+
88
+ # Collect one complete review packet under an exact head/base guard.
89
+ # Provider failure is propagated; empty diff/comments remain valid
90
+ # evidence only after the full inventory and second read agree.
91
+ def review_snapshot(identifier, include_comments: true)
92
+ reference = parse_identifier(identifier)
93
+ server = resolve_server_for(reference)
94
+ provider = provider_for(server)
95
+ before = provider.pull_request(number: reference.number)
96
+ head = required_head!(before)
97
+ details = provider.pull_request_review_details(number: reference.number)
98
+ diff = provider.pull_request_diff(number: reference.number)
99
+ unless diff.is_a?(String)
100
+ raise ProviderMalformedOutputError, "PR diff is not text"
101
+ end
102
+ comments = if include_comments
103
+ provider.pull_request_review_evidence(number: reference.number, expected_head: head)
104
+ else
105
+ ProviderReviewEvidence.new(
106
+ server_name: server.name, repository_url: server.url,
107
+ pr_number: before.number, head_sha: head, comments: [], reviews: []
108
+ )
109
+ end
110
+ checks = provider.pull_request_checks(number: reference.number, head_sha: head)
111
+ # The CLI-parsed PR may omit the description; hydrate it from the
112
+ # provider's body capability when available (nil stays absence).
113
+ # Hydration is itself a remote read: the final identity comes from
114
+ # the read that follows it, so body and identity can never come
115
+ # from different points in time.
116
+ body_before = provider.pull_request_body(number: reference.number)
117
+ after = provider.pull_request(number: reference.number)
118
+ body_after = provider.pull_request_body(number: reference.number)
119
+ if body_before != body_after
120
+ raise ProviderExpectedHeadConflictError,
121
+ "PR body changed while collecting review evidence; retry on the current head"
122
+ end
123
+ after = after.with(body: body_after) unless body_after.nil?
124
+ latest_details = provider.pull_request_review_details(number: reference.number)
125
+ if after.head_sha != head || latest_details.base_sha != details.base_sha ||
126
+ after.head_ref != before.head_ref || after.base_ref != before.base_ref ||
127
+ after.head_repository_url != before.head_repository_url ||
128
+ after.base_repository_url != before.base_repository_url
129
+ raise ProviderExpectedHeadConflictError,
130
+ "PR head/base changed while collecting review evidence; retry on the current head"
131
+ end
132
+ unless details.files.is_a?(Array) && checks.is_a?(Array) &&
133
+ comments.head_sha == head && comments.pr_number == before.number
134
+ raise ProviderMalformedOutputError, "PR review evidence is incomplete or mismatched"
135
+ end
136
+ ProviderReviewSnapshot.new(
137
+ provider: server.provider, pull_request: after,
138
+ base_sha: details.base_sha, files: details.files,
139
+ diff: diff, review_evidence: comments, checks: checks
140
+ )
141
+ end
142
+
143
+ def post_review_comment(identifier, expected_head:, body:, correlation:)
144
+ reference = parse_identifier(identifier)
145
+ provider = provider_for(resolve_server_for(reference))
146
+ provider.create_pull_request_comment(
147
+ number: reference.number, expected_head: expected_head,
148
+ body: body, correlation: correlation
149
+ )
150
+ end
151
+
152
+ def update_review_comment(identifier, expected_head:, comment_id:, body:)
153
+ reference = parse_identifier(identifier)
154
+ provider = provider_for(resolve_server_for(reference))
155
+ provider.update_pull_request_comment(
156
+ number: reference.number, expected_head: expected_head,
157
+ comment_id: comment_id, body: body
158
+ )
159
+ end
160
+
161
+ def resolve_review_thread(identifier, expected_head:, thread_id:)
162
+ reference = parse_identifier(identifier)
163
+ provider = provider_for(resolve_server_for(reference))
164
+ provider.resolve_pull_request_thread(
165
+ number: reference.number, expected_head: expected_head,
166
+ thread_id: thread_id
167
+ )
168
+ end
169
+
170
+ def file_at_ref(identifier, path:, ref:)
171
+ reference = parse_identifier(identifier)
172
+ provider = provider_for(resolve_server_for(reference))
173
+ provider.repository_file(path: path, ref: ref)
174
+ end
175
+
48
176
  # Create (or reconcile to) a pull request for an exact base/head
49
177
  # identity. There is no identifier: the selected server's repository
50
178
  # is the base, `head_repository_url` names the source (defaults to
@@ -106,8 +234,19 @@ module Ace
106
234
  )
107
235
  end
108
236
 
237
+ def resolved_server_url(identifier)
238
+ reference = parse_identifier(identifier)
239
+ resolve_server_for(reference).url
240
+ end
109
241
  private
110
242
 
243
+ def required_head!(pull_request)
244
+ head = pull_request.head_sha
245
+ return head if head.to_s.match?(/\A[0-9a-f]{40}\z/)
246
+
247
+ raise ProviderMalformedOutputError, "PR evidence is missing an exact head SHA"
248
+ end
249
+
111
250
  def parse_identifier(identifier)
112
251
  reference = Atoms::PrReference.parse(identifier)
113
252
  raise ArgumentError, "Invalid PR identifier: #{identifier}" unless reference
@@ -154,7 +293,11 @@ module Ace
154
293
  end
155
294
  end
156
295
 
296
+ # The forge server URL for a PR identifier, resolved on demand
297
+ # through the shared registry (pure resolution, no mutation).
298
+
157
299
  def provider_for(server)
300
+ @resolved_server_url = server.url
158
301
  Ace::Git::Providers.for(server, timeout: @timeout, runner: @runner)
159
302
  end
160
303
 
@@ -104,6 +104,50 @@ module Ace
104
104
  raise NotImplementedError, "Providers must implement #{self.class}#repository"
105
105
  end
106
106
 
107
+ # Collect normalized conversation comments, code comments and reviews
108
+ # for one exact PR head. A provider must reject malformed collections;
109
+ # an empty successful collection is represented by empty arrays.
110
+ def pull_request_review_evidence(number:, expected_head:)
111
+ raise NotImplementedError, "Providers must implement #{self.class}#pull_request_review_evidence"
112
+ end
113
+
114
+ # The PR description text when the provider can supply it; nil stays
115
+ # an honest absence for providers whose CLI surface omits the body.
116
+ def pull_request_body(number:)
117
+ nil
118
+ end
119
+
120
+ # The exact base SHA and complete file inventory used to prove that a
121
+ # review diff has not been silently truncated.
122
+ def pull_request_review_details(number:)
123
+ raise NotImplementedError, "Providers must implement #{self.class}#pull_request_review_details"
124
+ end
125
+
126
+ def pull_request_checks(number:, head_sha:)
127
+ raise NotImplementedError, "Providers must implement #{self.class}#pull_request_checks"
128
+ end
129
+
130
+ def repository_file(path:, ref:)
131
+ raise NotImplementedError, "Providers must implement #{self.class}#repository_file"
132
+ end
133
+
134
+ # Create a top-level PR comment. `correlation` is a stable session key
135
+ # embedded in the body so a retry can find exactly one prior send.
136
+ # A possible post-send transport failure raises ProviderUnknownOutcomeError.
137
+ def create_pull_request_comment(number:, expected_head:, body:, correlation:)
138
+ raise NotImplementedError, "Providers must implement #{self.class}#create_pull_request_comment"
139
+ end
140
+
141
+ def update_pull_request_comment(number:, expected_head:, comment_id:, body:)
142
+ raise NotImplementedError, "Providers must implement #{self.class}#update_pull_request_comment"
143
+ end
144
+
145
+ # Thread resolution is a separate capability. Unsupported providers
146
+ # must raise ProviderUnsupportedCapabilityError, never simulate success.
147
+ def resolve_pull_request_thread(number:, expected_head:, thread_id:)
148
+ raise NotImplementedError, "Providers must implement #{self.class}#resolve_pull_request_thread"
149
+ end
150
+
107
151
  # ---- PR lifecycle mutations ----
108
152
  #
109
153
  # Contract requirements (enforced by every provider implementation):
@@ -11,7 +11,7 @@ module Ace
11
11
  # Pull request state values: :open, :merged, :closed. Issue state values:
12
12
  # :open, :closed.
13
13
  ProviderPullRequest = Data.define(
14
- :server_name, :number, :title, :state, :head_ref, :base_ref,
14
+ :server_name, :number, :title, :body, :state, :head_ref, :base_ref,
15
15
  :head_sha, :author, :url, :draft, :merged_at,
16
16
  :head_repository_url, :base_repository_url, :merge_commit_sha
17
17
  )
@@ -21,6 +21,33 @@ module Ace
21
21
  # Normalized check/CI evidence for one check run.
22
22
  ProviderCheck = Data.define(:server_name, :name, :state, :conclusion, :url)
23
23
 
24
+ # Comments keep the exact repository and PR identity supplied by the
25
+ # selected provider. A nil path/line denotes a conversation comment.
26
+ ProviderReviewComment = Data.define(
27
+ :server_name, :repository_url, :pr_number, :id, :author, :body,
28
+ :url, :path, :line, :head_sha, :resolved, :thread_id
29
+ )
30
+
31
+ ProviderReview = Data.define(
32
+ :server_name, :repository_url, :pr_number, :id, :author, :body,
33
+ :state, :url, :head_sha
34
+ )
35
+
36
+ ProviderReviewEvidence = Data.define(
37
+ :server_name, :repository_url, :pr_number, :head_sha, :comments, :reviews
38
+ )
39
+
40
+ ProviderReviewDetails = Data.define(:server_name, :repository_url, :pr_number, :base_sha, :files)
41
+
42
+ ProviderReviewSnapshot = Data.define(
43
+ :provider, :pull_request, :base_sha, :files, :diff, :review_evidence, :checks
44
+ )
45
+
46
+ ProviderReviewMutation = Data.define(
47
+ :server_name, :repository_url, :pr_number, :head_sha, :comment,
48
+ :idempotency
49
+ )
50
+
24
51
  # Normalized repository evidence.
25
52
  ProviderRepository = Data.define(:server_name, :full_name, :default_branch, :url)
26
53
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Git
5
- VERSION = "0.25.0"
5
+ VERSION = "0.26.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-git
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.0
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-29 00:00:00.000000000 Z
10
+ date: 2026-10-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-config