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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/ace/git/organisms/pull_request_lifecycle.rb +143 -0
- data/lib/ace/git/providers/base.rb +44 -0
- data/lib/ace/git/providers/evidence.rb +28 -1
- data/lib/ace/git/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b306e7eeb68db0b211dead9498aabdeba48ca866b13a3710a886193232d310a
|
|
4
|
+
data.tar.gz: db3448da7471947520a0aa68f70c56283f3539412456b221809ab7cd0349f976
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/ace/git/version.rb
CHANGED
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.
|
|
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-
|
|
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
|