ace-review 0.57.1 → 0.58.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/.ace-defaults/review/config.yml +2 -2
- data/CHANGELOG.md +6 -3
- data/docs/feedback-workflow.md +4 -4
- data/docs/getting-started.md +1 -1
- data/docs/usage.md +3 -2
- data/lib/ace/review/atoms/pr_comment_formatter.rb +36 -5
- data/lib/ace/review/cli/commands/review.rb +16 -11
- data/lib/ace/review/errors.rb +0 -68
- data/lib/ace/review/models/review_options.rb +6 -3
- data/lib/ace/review/molecules/campaign_evidence.rb +10 -19
- data/lib/ace/review/molecules/pr_provider.rb +157 -0
- data/lib/ace/review/organisms/campaign_manager.rb +29 -13
- data/lib/ace/review/organisms/review_manager.rb +184 -78
- data/lib/ace/review/version.rb +1 -1
- data/lib/ace/review.rb +1 -4
- metadata +20 -10
- data/lib/ace/review/atoms/retry_with_backoff.rb +0 -75
- data/lib/ace/review/molecules/gh_comment_poster.rb +0 -207
- data/lib/ace/review/molecules/gh_comment_resolver.rb +0 -201
- data/lib/ace/review/molecules/gh_pr_comment_fetcher.rb +0 -410
- data/lib/ace/review/molecules/gh_pr_fetcher.rb +0 -307
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fda3bb688af1e2d4c71e6f37dc20d25c4f756703c3a2910827629975fd0965e1
|
|
4
|
+
data.tar.gz: a479d205b501a785063680654466e0eda84e5d00397cc74831dbffc24d106552
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5d61e56ef9b7c8cd02a8c44130e4c0d43266d3327290e963136f36b442a596d6070b9736d94f46427265be4696bc43c4d0e8c69142eaa4fd5019a0e6db31b9c
|
|
7
|
+
data.tar.gz: 0dde4d996c5f713b5f5babfe57f03814cfe658d217207071da16f8248e387e14bad4916184e8175c0f603842efac319636a73d5e1394b6b92a7c29dea072b208
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# context - Default context preset (default: "project")
|
|
10
10
|
# max_concurrent_models - Max parallel LLM queries for multi-model execution (default: 3)
|
|
11
11
|
# auto_execute - Auto-execute LLM query without confirmation (default: false)
|
|
12
|
-
#
|
|
12
|
+
# provider_timeout - Timeout for forge operations in seconds (default: 30)
|
|
13
13
|
# gh_simple_timeout - Timeout for simple gh commands like --version, auth status (default: 10)
|
|
14
14
|
# llm_timeout - Timeout for LLM queries in seconds (default: 900)
|
|
15
15
|
#
|
|
@@ -37,7 +37,7 @@ defaults:
|
|
|
37
37
|
bundle: "project"
|
|
38
38
|
max_concurrent_models: 3 # Max parallel LLM queries for multi-model execution
|
|
39
39
|
auto_execute: false # Set to true to auto-execute LLM query without confirmation
|
|
40
|
-
|
|
40
|
+
provider_timeout: 30 # Timeout for forge operations in seconds
|
|
41
41
|
gh_simple_timeout: 10 # Timeout for simple gh commands (--version, auth status)
|
|
42
42
|
llm_timeout: 900 # Timeout for LLM queries in seconds (15 minutes)
|
|
43
43
|
|
data/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.
|
|
10
|
+
## [0.58.0] - 2026-10-02
|
|
11
11
|
|
|
12
|
-
###
|
|
12
|
+
### Added
|
|
13
|
+
- Provider-neutral PR reviews and comment handling for GitHub, default Forgejo, and named Forgejo servers with exact-head evidence.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
### Changed
|
|
16
|
+
- Replaced `--gh-timeout` with `--provider-timeout`; removed the GitHub-only review fetcher, poster, and resolver paths.
|
|
17
|
+
- Preserve provider and repository identity across review sessions and comments; fail closed on moved heads and collection errors.
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
## [0.57.0] - 2026-10-01
|
data/docs/feedback-workflow.md
CHANGED
|
@@ -23,7 +23,7 @@ Key benefits:
|
|
|
23
23
|
|
|
24
24
|
1. Run a review with feedback extraction:
|
|
25
25
|
```bash
|
|
26
|
-
ace-review --preset code-
|
|
26
|
+
ace-review --preset code-valid --pr 123 --auto-execute
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
2. List feedback items:
|
|
@@ -245,7 +245,7 @@ feedback:
|
|
|
245
245
|
### Example Workflow Configuration
|
|
246
246
|
|
|
247
247
|
```yaml
|
|
248
|
-
# .ace/review/presets/code-
|
|
248
|
+
# .ace/review/presets/code-valid.yml
|
|
249
249
|
presets:
|
|
250
250
|
- code
|
|
251
251
|
|
|
@@ -416,10 +416,10 @@ Each review execution creates a new session:
|
|
|
416
416
|
|
|
417
417
|
```bash
|
|
418
418
|
# First review
|
|
419
|
-
ace-review --pr 189 --preset code-
|
|
419
|
+
ace-review --pr 189 --preset code-valid
|
|
420
420
|
|
|
421
421
|
# Second review (different PR or updated code)
|
|
422
|
-
ace-review --pr 190 --preset code-
|
|
422
|
+
ace-review --pr 190 --preset code-valid
|
|
423
423
|
|
|
424
424
|
# List all sessions
|
|
425
425
|
ls -la .ace-local/review/sessions/
|
data/docs/getting-started.md
CHANGED
|
@@ -38,7 +38,7 @@ This runs a code-focused review against your current branch diff and writes sess
|
|
|
38
38
|
Run:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
ace-review --pr 123 --preset code-
|
|
41
|
+
ace-review --pr 123 --preset code-valid --auto-execute
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Use `--post-comment` when you want to publish the review back to GitHub.
|
data/docs/usage.md
CHANGED
|
@@ -29,7 +29,7 @@ ace-review [OPTIONS]
|
|
|
29
29
|
|
|
30
30
|
| Option | Description |
|
|
31
31
|
|--------|-------------|
|
|
32
|
-
| `--preset` | Review preset
|
|
32
|
+
| `--preset` | Review preset from the shipped catalog, such as code-valid, code-fit, integration, or docs |
|
|
33
33
|
| `--pr` | Review GitHub PR (number, URL, or owner/repo#number) |
|
|
34
34
|
| `--subject` | Subject config (repeatable): `diff:range`, `diff:range -- path`, `files:glob`, `preset:name` |
|
|
35
35
|
| `--context` | Context config (preset name or YAML) |
|
|
@@ -43,7 +43,8 @@ ace-review [OPTIONS]
|
|
|
43
43
|
|--------|-------------|
|
|
44
44
|
| `--pr-comments` | Include PR comments as feedback source (default: true for --pr) |
|
|
45
45
|
| `--post-comment` | Post review as PR comment (requires --pr) |
|
|
46
|
-
| `--
|
|
46
|
+
| `--server` / `--default-server` | Select a configured forge server for a PR review |
|
|
47
|
+
| `--provider-timeout` | Timeout for forge operations in seconds (default: 30) |
|
|
47
48
|
|
|
48
49
|
### Prompt Composition
|
|
49
50
|
|
|
@@ -20,20 +20,37 @@ module Ace
|
|
|
20
20
|
|
|
21
21
|
# Format comments data into markdown report
|
|
22
22
|
#
|
|
23
|
-
# @param comments_data [Hash]
|
|
23
|
+
# @param comments_data [Hash] normalized provider comment data
|
|
24
24
|
# @return [String] Formatted markdown report
|
|
25
25
|
def self.format(comments_data)
|
|
26
26
|
return nil unless comments_data && comments_data[:success]
|
|
27
27
|
|
|
28
28
|
pr_number = comments_data[:pr_number]
|
|
29
29
|
pr_title = comments_data[:pr_title]
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
# Full provider evidence is preserved upstream; prompt feedback
|
|
31
|
+
# excludes bot posts, blank bodies, and our own session comments
|
|
32
|
+
# (they are not unresolved human feedback).
|
|
33
|
+
comments = (comments_data[:comments] || []).reject do |comment|
|
|
34
|
+
author = comment[:author].to_s
|
|
35
|
+
body = comment[:body].to_s
|
|
36
|
+
author.match?(/\b(bot|github-actions|ace-review)\b/i) ||
|
|
37
|
+
body.empty? ||
|
|
38
|
+
own_session_post?(body)
|
|
39
|
+
end
|
|
40
|
+
reviews = (comments_data[:reviews] || []).reject do |review|
|
|
41
|
+
review[:author].to_s.match?(/\b(bot|github-actions|ace-review)\b/i)
|
|
42
|
+
end
|
|
32
43
|
review_threads = comments_data[:review_threads] || []
|
|
33
44
|
|
|
34
45
|
# Build report sections
|
|
35
46
|
frontmatter = build_frontmatter(comments_data)
|
|
36
47
|
summary = build_summary(comments, reviews, review_threads, pr_number, pr_title)
|
|
48
|
+
# Apply the same bot filter to thread comments; threads left
|
|
49
|
+
# without human comments are omitted from feedback.
|
|
50
|
+
review_threads = review_threads.map do |thread|
|
|
51
|
+
filtered = thread.merge(comments: Array(thread[:comments]).reject { |c| c[:author].to_s.match?(/\b(bot|github-actions|ace-review)\b/i) })
|
|
52
|
+
filtered.merge(comments: filtered[:comments])
|
|
53
|
+
end.reject { |thread| Array(thread[:comments]).empty? }
|
|
37
54
|
inline_section = build_inline_comments_section(review_threads)
|
|
38
55
|
unresolved_section = build_unresolved_section(comments, reviews)
|
|
39
56
|
resolved_section = build_resolved_section(reviews)
|
|
@@ -76,6 +93,7 @@ module Ace
|
|
|
76
93
|
inline_thread_count = review_threads.size
|
|
77
94
|
reviewers = extract_reviewers(comments, reviews, review_threads)
|
|
78
95
|
unresolved_count = count_unresolved(comments, reviews, review_threads)
|
|
96
|
+
unknown_resolution = review_threads.count { |t| t[:is_resolved].nil? }
|
|
79
97
|
|
|
80
98
|
summary = "# Developer Feedback from PR ##{pr_number}\n\n"
|
|
81
99
|
summary += "> #{pr_title}\n\n" if pr_title && !pr_title.empty?
|
|
@@ -83,6 +101,7 @@ module Ace
|
|
|
83
101
|
summary += "- Total comments: #{total_comments}\n"
|
|
84
102
|
summary += "- Inline code comments: #{inline_thread_count}\n" if inline_thread_count > 0
|
|
85
103
|
summary += "- Unresolved items: #{unresolved_count}\n"
|
|
104
|
+
summary += "- Unknown-resolution threads: #{unknown_resolution}\n" if unknown_resolution > 0
|
|
86
105
|
summary += "- Reviewers: #{reviewers.map { |r| "@#{r}" }.join(", ")}\n" if reviewers.any?
|
|
87
106
|
summary
|
|
88
107
|
end
|
|
@@ -144,7 +163,11 @@ module Ace
|
|
|
144
163
|
line = thread[:line]
|
|
145
164
|
thread_id = thread[:id]
|
|
146
165
|
is_resolved = thread[:is_resolved]
|
|
147
|
-
status = is_resolved
|
|
166
|
+
status = case is_resolved
|
|
167
|
+
when true then "Resolved"
|
|
168
|
+
when false then "Unresolved"
|
|
169
|
+
else "Resolution unknown"
|
|
170
|
+
end
|
|
148
171
|
|
|
149
172
|
# Header with file:line, thread ID, and status
|
|
150
173
|
location = line ? "#{path}:#{line}" : path
|
|
@@ -186,6 +209,14 @@ module Ace
|
|
|
186
209
|
section
|
|
187
210
|
end
|
|
188
211
|
|
|
212
|
+
# An ACE post carries our review header AND a well-formed session
|
|
213
|
+
# marker as the trailing hidden line. A human reply quoting an
|
|
214
|
+
# earlier comment rarely matches both, so quotes stay visible.
|
|
215
|
+
def self.own_session_post?(body)
|
|
216
|
+
return false unless body.start_with?("## Code Review - ace-review")
|
|
217
|
+
body.match?(/<!--\s*ace-review-session:[a-zA-Z0-9._:-]+\s*-->\s*\z/)
|
|
218
|
+
end
|
|
219
|
+
|
|
189
220
|
# Build comments table for quick reference
|
|
190
221
|
#
|
|
191
222
|
# @param comments [Array<Hash>] Issue comments
|
|
@@ -256,7 +287,7 @@ module Ace
|
|
|
256
287
|
# @param review_threads [Array<Hash>] Inline review threads
|
|
257
288
|
# @return [Integer] Count of unresolved items
|
|
258
289
|
def self.count_unresolved(comments, reviews, review_threads = [])
|
|
259
|
-
unresolved_threads = review_threads.count { |t|
|
|
290
|
+
unresolved_threads = review_threads.count { |t| t[:is_resolved] == false }
|
|
260
291
|
actionable_comments = comments.count { |c| actionable_comment?(c[:body]) }
|
|
261
292
|
actionable_comments + reviews.count { |r| r[:state] == "CHANGES_REQUESTED" } + unresolved_threads
|
|
262
293
|
end
|
|
@@ -16,10 +16,11 @@ module Ace
|
|
|
16
16
|
Execute code review using presets or custom configuration
|
|
17
17
|
|
|
18
18
|
Presets provide pre-configured review types with focused prompts:
|
|
19
|
-
code
|
|
20
|
-
code-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
code-valid → Code correctness review
|
|
20
|
+
code-fit → Code design review
|
|
21
|
+
code-shine → Code quality review
|
|
22
|
+
integration → Integration review
|
|
23
|
+
spec → Specification review
|
|
23
24
|
docs → Documentation review
|
|
24
25
|
|
|
25
26
|
Configuration:
|
|
@@ -31,13 +32,13 @@ module Ace
|
|
|
31
32
|
DESC
|
|
32
33
|
|
|
33
34
|
example [
|
|
34
|
-
"--preset code-pr
|
|
35
|
-
"--preset
|
|
35
|
+
"--preset code-valid --pr 123 # PR code review",
|
|
36
|
+
"--preset code-fit --auto-execute # Run and apply fixes",
|
|
36
37
|
"--pr 123 # Review by PR number",
|
|
37
38
|
"--pr 123 --delta # Delta round since the most recent prior session's head",
|
|
38
39
|
"--pr 123 --delta <head> # Delta round since an explicit reviewed head",
|
|
39
|
-
"--preset code --subject diff:HEAD~3 --subject files:docs/**/*.md",
|
|
40
|
-
"--preset
|
|
40
|
+
"--preset code-valid --subject diff:HEAD~3 --subject files:docs/**/*.md",
|
|
41
|
+
"--preset code-fit --dry-run # Preview without executing"
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
# Review configuration options
|
|
@@ -58,10 +59,12 @@ module Ace
|
|
|
58
59
|
option :auto_execute, type: :boolean, default: nil, desc: "Execute LLM query automatically"
|
|
59
60
|
option :save_session, type: :boolean, desc: "Save session files (default: true)"
|
|
60
61
|
option :session_dir, type: :string, desc: "Custom session directory"
|
|
61
|
-
option :pr, type: :string, desc: "Review
|
|
62
|
+
option :pr, type: :string, desc: "Review PR (number, URL, or owner/repo#number)"
|
|
63
|
+
option :server, type: :string, desc: "Use the named configured forge server"
|
|
64
|
+
option :default_server, type: :boolean, desc: "Use the configured default forge server"
|
|
62
65
|
option :pr_comments, type: :boolean, desc: "Include PR comments as feedback source (default: true for --pr)"
|
|
63
66
|
option :post_comment, type: :boolean, desc: "Post review as PR comment (requires --pr)"
|
|
64
|
-
option :
|
|
67
|
+
option :provider_timeout, type: :integer, desc: "Timeout for forge operations in seconds (default: 30)"
|
|
65
68
|
option :evidence_session, type: :array, desc: "Include selected prior review sessions as context"
|
|
66
69
|
option :delta, type: :string, optional_value: true,
|
|
67
70
|
desc: "Delta round for --pr: review only changes since a reference head (explicit head, or `auto` to auto-resolve from the most recent prior session for this PR); omit for a full round"
|
|
@@ -106,7 +109,9 @@ module Ace
|
|
|
106
109
|
end
|
|
107
110
|
|
|
108
111
|
# Type-convert numeric options (ace-support-cli returns strings, Thor converted to integers)
|
|
109
|
-
|
|
112
|
+
if cli_options[:provider_timeout]
|
|
113
|
+
cli_options[:provider_timeout] = cli_options[:provider_timeout].to_i
|
|
114
|
+
end
|
|
110
115
|
|
|
111
116
|
# Build and store options for testing compatibility
|
|
112
117
|
@options = build_review_options(cli_options)
|
data/lib/ace/review/errors.rb
CHANGED
|
@@ -32,74 +32,6 @@ module Ace
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Raised when gh CLI is not installed
|
|
36
|
-
class GhCliNotInstalledError < Error
|
|
37
|
-
def initialize
|
|
38
|
-
message = "GitHub CLI (gh) is not installed.\n"
|
|
39
|
-
message += "Install with:\n"
|
|
40
|
-
message += " macOS: brew install gh\n"
|
|
41
|
-
message += " Linux: See https://cli.github.com/manual/installation\n"
|
|
42
|
-
message += " Windows: See https://cli.github.com/manual/installation"
|
|
43
|
-
|
|
44
|
-
super(message)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Raised when user is not authenticated with GitHub
|
|
49
|
-
class GhAuthenticationError < Error
|
|
50
|
-
def initialize
|
|
51
|
-
message = "GitHub authentication required.\n"
|
|
52
|
-
message += "Run 'gh auth login' to authenticate with GitHub.\n"
|
|
53
|
-
message += "Check status: gh auth status"
|
|
54
|
-
|
|
55
|
-
super(message)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Raised when a PR is not found
|
|
60
|
-
class PrNotFoundError < Error
|
|
61
|
-
attr_reader :pr_identifier
|
|
62
|
-
|
|
63
|
-
def initialize(pr_identifier, details = nil)
|
|
64
|
-
@pr_identifier = pr_identifier
|
|
65
|
-
message = "Pull request '#{pr_identifier}' not found."
|
|
66
|
-
message += "\n#{details}" if details
|
|
67
|
-
|
|
68
|
-
super(message)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Raised when PR diff exceeds GitHub's API size limit (300 files)
|
|
73
|
-
class DiffTooLargeError < Error
|
|
74
|
-
attr_reader :pr_identifier
|
|
75
|
-
|
|
76
|
-
def initialize(pr_identifier, details = nil)
|
|
77
|
-
@pr_identifier = pr_identifier
|
|
78
|
-
message = "Pull request '#{pr_identifier}' diff exceeds GitHub API size limit."
|
|
79
|
-
message += "\n#{details}" if details
|
|
80
|
-
|
|
81
|
-
super(message)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Raised when attempting to post to a closed/merged PR
|
|
86
|
-
class PrStateError < Error
|
|
87
|
-
attr_reader :pr_number, :state
|
|
88
|
-
|
|
89
|
-
def initialize(pr_number, state)
|
|
90
|
-
@pr_number = pr_number
|
|
91
|
-
@state = state
|
|
92
|
-
|
|
93
|
-
message = "Cannot post comment to PR ##{pr_number}.\n"
|
|
94
|
-
message += "PR is in '#{state}' state. Comments can only be posted to open PRs."
|
|
95
|
-
|
|
96
|
-
super(message)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Raised when gh CLI encounters a network error
|
|
101
|
-
class GhNetworkError < Error; end
|
|
102
|
-
|
|
103
35
|
# Raised when ace-task cannot find task
|
|
104
36
|
class TaskNotFoundError < Error
|
|
105
37
|
attr_reader :task_ref
|
|
@@ -9,7 +9,8 @@ module Ace
|
|
|
9
9
|
:prompt_base, :prompt_format, :prompt_focus, :add_focus,
|
|
10
10
|
:prompt_guidelines, :model, :models, :dry_run, :verbose,
|
|
11
11
|
:auto_execute, :save_session, :session_dir,
|
|
12
|
-
:pr, :post_comment, :pr_metadata, :
|
|
12
|
+
:pr, :post_comment, :pr_metadata, :provider_timeout,
|
|
13
|
+
:server, :default_server,
|
|
13
14
|
:pr_comments, :pr_comment_data,
|
|
14
15
|
:evidence_sessions, :campaign, :campaign_round, :campaign_scope,
|
|
15
16
|
:delta,
|
|
@@ -54,7 +55,9 @@ module Ace
|
|
|
54
55
|
@pr = hash[:pr]
|
|
55
56
|
@post_comment = hash[:post_comment] || false
|
|
56
57
|
@pr_metadata = hash[:pr_metadata]
|
|
57
|
-
@
|
|
58
|
+
@provider_timeout = hash[:provider_timeout]
|
|
59
|
+
@server = hash[:server]
|
|
60
|
+
@default_server = hash[:default_server] || false
|
|
58
61
|
|
|
59
62
|
# PR comment options
|
|
60
63
|
@pr_comments = hash[:pr_comments] # nil = use default, true/false = explicit
|
|
@@ -195,7 +198,7 @@ module Ace
|
|
|
195
198
|
end
|
|
196
199
|
end
|
|
197
200
|
|
|
198
|
-
@
|
|
201
|
+
@provider_timeout ||= config["provider_timeout"]
|
|
199
202
|
|
|
200
203
|
self
|
|
201
204
|
end
|
|
@@ -18,16 +18,6 @@ module Ace
|
|
|
18
18
|
class CampaignEvidence
|
|
19
19
|
Contract = Atoms::CampaignContract
|
|
20
20
|
|
|
21
|
-
# One completion predicate for provider executions, shared with the
|
|
22
|
-
# extraction-inventory writer so session metadata and campaign
|
|
23
|
-
# evidence can never disagree about which reports were reviewed.
|
|
24
|
-
def self.completed_entry?(entry)
|
|
25
|
-
execution = entry["execution"] || {}
|
|
26
|
-
entry["status"] == "success" && execution["status"].to_s == "succeeded" &&
|
|
27
|
-
!execution["provider"].to_s.empty? && !execution["model"].to_s.empty? &&
|
|
28
|
-
!entry["completed_at"].to_s.empty?
|
|
29
|
-
end
|
|
30
|
-
|
|
31
21
|
def initialize(repo_root:, check_evidence: nil, review_evidence: nil, approval_evidence: nil)
|
|
32
22
|
@repo_root = File.realpath(repo_root)
|
|
33
23
|
authority = CampaignExecutionEvidence.new(repo_root: @repo_root)
|
|
@@ -67,9 +57,9 @@ module Ace
|
|
|
67
57
|
metadata = json_keys(YAML.safe_load_file(metadata_path, permitted_classes: [Time, Date, Symbol]))
|
|
68
58
|
Contract.object!(metadata, "session metadata")
|
|
69
59
|
expected = {"campaign_id" => record["id"], "contract_identity" => record["contract_identity"],
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
"subject" => record["subject"], "round_id" => binding["round_id"], "scope" => scope,
|
|
61
|
+
"head" => binding["head"], "base" => binding["base"],
|
|
62
|
+
"scope_identity" => binding["scope_identity"][scope]}
|
|
73
63
|
unless metadata["campaign_binding"] == expected && metadata["preset"] == expected["scope_identity"]["preset"]
|
|
74
64
|
raise Contract::Invalid, "session campaign/contract/scope/head/base identity mismatch"
|
|
75
65
|
end
|
|
@@ -93,7 +83,12 @@ module Ace
|
|
|
93
83
|
reports = []
|
|
94
84
|
entries.each do |entry|
|
|
95
85
|
Contract.object!(entry, "model execution")
|
|
96
|
-
|
|
86
|
+
execution = entry["execution"] || {}
|
|
87
|
+
complete = entry["status"] == "success" && execution["status"] == "succeeded" &&
|
|
88
|
+
!execution["provider"].to_s.empty? && !execution["model"].to_s.empty? &&
|
|
89
|
+
!entry["completed_at"].to_s.empty?
|
|
90
|
+
completed &&= complete
|
|
91
|
+
next unless complete
|
|
97
92
|
path = entry["output_file"].to_s
|
|
98
93
|
raise Contract::Invalid, "completed execution has no report" if path.empty?
|
|
99
94
|
path = File.expand_path(path, dir)
|
|
@@ -104,7 +99,7 @@ module Ace
|
|
|
104
99
|
resolved, report_ref = artifact("path" => path, "sha256" => entry["report_sha256"])
|
|
105
100
|
raise Contract::Invalid, "empty reviewer report" if File.read(resolved).strip.empty?
|
|
106
101
|
refs << report_ref
|
|
107
|
-
reports << {"artifact" => report_ref, "execution" =>
|
|
102
|
+
reports << {"artifact" => report_ref, "execution" => execution}
|
|
108
103
|
prompts = entry["prompt_sha256"]
|
|
109
104
|
Contract.object!(prompts, "execution prompt checksums")
|
|
110
105
|
%w[system user].each do |name|
|
|
@@ -122,10 +117,6 @@ module Ace
|
|
|
122
117
|
raise Contract::Invalid, "feedback extraction inventory or reviewed reports changed"
|
|
123
118
|
end
|
|
124
119
|
end
|
|
125
|
-
# One completed reviewer per needed scope suffices: a failed or
|
|
126
|
-
# incomplete provider entry does not count as a reviewer, but it
|
|
127
|
-
# does not invalidate the model executions that did complete.
|
|
128
|
-
completed &&= reports.any?
|
|
129
120
|
completed &&= extracted
|
|
130
121
|
proof = completed ? @review_evidence.call(input.fetch("receipt"), head: binding["head"], artifacts: refs) : nil
|
|
131
122
|
{"receipt" => input["receipt"], "execution_proof" => proof, "path" => dir.delete_prefix(@repo_root + File::SEPARATOR), "scope" => scope,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/git"
|
|
4
|
+
require "ace/git/github"
|
|
5
|
+
require "ace/git/forgejo"
|
|
6
|
+
require "ace/git/organisms/pull_request_lifecycle"
|
|
7
|
+
require "digest"
|
|
8
|
+
|
|
9
|
+
module Ace
|
|
10
|
+
module Review
|
|
11
|
+
module Molecules
|
|
12
|
+
# Review-facing projection of the shared, exact-head provider contract.
|
|
13
|
+
# Provider commands and response parsing remain in ace-git packages.
|
|
14
|
+
class PrProvider
|
|
15
|
+
def self.format_comment(review_text, preset: nil, model: nil, timestamp: nil)
|
|
16
|
+
header = "## Code Review - ace-review\n\n"
|
|
17
|
+
header += "**Preset**: #{preset}\n" if preset
|
|
18
|
+
header += "**Model**: #{model}\n" if model
|
|
19
|
+
header += "**Generated**: #{timestamp}\n" if timestamp
|
|
20
|
+
header += "\n" if preset || model || timestamp
|
|
21
|
+
content = review_text.to_s
|
|
22
|
+
content += "\n```\n" if content.scan(/^```/).count.odd?
|
|
23
|
+
header + <<~MARKDOWN
|
|
24
|
+
<details>
|
|
25
|
+
<summary><b>Full Review</b> (click to expand)</summary>
|
|
26
|
+
|
|
27
|
+
#{content}
|
|
28
|
+
|
|
29
|
+
</details>
|
|
30
|
+
MARKDOWN
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The forge server URL resolved by the lifecycle at construction;
|
|
34
|
+
# posting compares it with the reviewed repository before mutating.
|
|
35
|
+
def resolved_server_url(pr_identifier)
|
|
36
|
+
@lifecycle.resolved_server_url(pr_identifier)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(server_name: nil, use_default: false, timeout: nil, runner: nil, lifecycle: nil)
|
|
40
|
+
@lifecycle = lifecycle || Ace::Git::Organisms::PullRequestLifecycle.new(
|
|
41
|
+
server_name: server_name, use_default: use_default,
|
|
42
|
+
timeout: timeout, runner: runner
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fetch(pr_identifier, include_comments: true)
|
|
47
|
+
snapshot = @lifecycle.review_snapshot(pr_identifier, include_comments: include_comments)
|
|
48
|
+
metadata = metadata_for(snapshot)
|
|
49
|
+
{
|
|
50
|
+
success: true,
|
|
51
|
+
diff: snapshot.diff,
|
|
52
|
+
metadata: metadata,
|
|
53
|
+
comments: comments_for(snapshot),
|
|
54
|
+
checks: snapshot.checks,
|
|
55
|
+
snapshot: snapshot
|
|
56
|
+
}
|
|
57
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
58
|
+
{success: false, error: "#{e.class.name.split('::').last}: #{e.message}"}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Exact-head PR metadata only (no diff, comments, or checks); the
|
|
62
|
+
# delta resolver needs identity and base provenance, never the
|
|
63
|
+
# whole diff.
|
|
64
|
+
def fetch_metadata(pr_identifier)
|
|
65
|
+
snapshot = @lifecycle.review_metadata_snapshot(pr_identifier)
|
|
66
|
+
{success: true, metadata: metadata_for(snapshot)}
|
|
67
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
68
|
+
{success: false, error: "#{e.class.name.split('::').last}: #{e.message}"}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def post_comment(pr_identifier, expected_head:, content:, session_key:)
|
|
72
|
+
correlation = Digest::SHA256.hexdigest(session_key.to_s)
|
|
73
|
+
@lifecycle.post_review_comment(
|
|
74
|
+
pr_identifier, expected_head: expected_head,
|
|
75
|
+
body: content, correlation: correlation
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_thread(pr_identifier, expected_head:, thread_id:)
|
|
80
|
+
@lifecycle.resolve_review_thread(
|
|
81
|
+
pr_identifier, expected_head: expected_head, thread_id: thread_id
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def update_comment(pr_identifier, expected_head:, comment_id:, body:)
|
|
86
|
+
@lifecycle.update_review_comment(
|
|
87
|
+
pr_identifier, expected_head: expected_head,
|
|
88
|
+
comment_id: comment_id, body: body
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def file_at_ref(pr_identifier, path:, ref:)
|
|
93
|
+
@lifecycle.file_at_ref(pr_identifier, path: path, ref: ref)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def metadata_for(snapshot)
|
|
99
|
+
pr = snapshot.pull_request
|
|
100
|
+
{
|
|
101
|
+
"server_name" => pr.server_name,
|
|
102
|
+
"provider" => snapshot.provider.to_s,
|
|
103
|
+
"repository_url" => pr.base_repository_url,
|
|
104
|
+
"number" => pr.number,
|
|
105
|
+
"title" => pr.title,
|
|
106
|
+
"body" => pr.body,
|
|
107
|
+
"author" => {"login" => pr.author},
|
|
108
|
+
"state" => pr.state.to_s.upcase,
|
|
109
|
+
"isDraft" => pr.draft,
|
|
110
|
+
"baseRefName" => pr.base_ref,
|
|
111
|
+
"baseRefOid" => snapshot.base_sha,
|
|
112
|
+
"headRefName" => pr.head_ref,
|
|
113
|
+
"headRefOid" => pr.head_sha,
|
|
114
|
+
"head_repository_url" => pr.head_repository_url,
|
|
115
|
+
"url" => pr.url,
|
|
116
|
+
"changedFiles" => snapshot.files.length,
|
|
117
|
+
"files" => snapshot.files.map { |path| {"path" => path} },
|
|
118
|
+
"checks" => snapshot.checks.map { |check| check.to_h.transform_keys(&:to_s) }
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def comments_for(snapshot)
|
|
123
|
+
pr = snapshot.pull_request
|
|
124
|
+
comments = snapshot.review_evidence.comments
|
|
125
|
+
issue_comments = comments.reject(&:path).map do |comment|
|
|
126
|
+
{type: "issue_comment", id: comment.id, author: comment.author,
|
|
127
|
+
body: comment.body, url: comment.url}
|
|
128
|
+
end
|
|
129
|
+
inline = comments.select(&:path)
|
|
130
|
+
# Comments sharing a provider thread id belong to one thread; a
|
|
131
|
+
# nil thread id stays an honest per-comment entry.
|
|
132
|
+
threads = inline.select(&:thread_id).group_by(&:thread_id).map do |id, members|
|
|
133
|
+
first = members.first
|
|
134
|
+
{id: id, path: first.path, line: first.line, is_resolved: first.resolved,
|
|
135
|
+
comments: members.map { |comment|
|
|
136
|
+
{id: comment.id, author: comment.author, body: comment.body, url: comment.url}
|
|
137
|
+
}}
|
|
138
|
+
end
|
|
139
|
+
threads += inline.reject(&:thread_id).map do |comment|
|
|
140
|
+
{id: nil, path: comment.path, line: comment.line, is_resolved: comment.resolved,
|
|
141
|
+
comments: [{id: comment.id, author: comment.author, body: comment.body, url: comment.url}]}
|
|
142
|
+
end
|
|
143
|
+
reviews = snapshot.review_evidence.reviews.map do |review|
|
|
144
|
+
{id: review.id, author: review.author, body: review.body,
|
|
145
|
+
state: review.state.to_s.upcase, url: review.url}
|
|
146
|
+
end
|
|
147
|
+
{
|
|
148
|
+
success: true, comments: issue_comments, review_threads: threads,
|
|
149
|
+
reviews: reviews, pr_number: pr.number, pr_title: pr.title,
|
|
150
|
+
pr_author: pr.author, server_name: pr.server_name,
|
|
151
|
+
repository_url: pr.base_repository_url, head_sha: pr.head_sha
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|