ace-review 0.58.0 → 0.58.1
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/presets/code-fit.yml +4 -3
- data/.ace-defaults/review/presets/code-shine.yml +1 -2
- data/.ace-defaults/review/presets/code-valid.yml +4 -3
- data/CHANGELOG.md +15 -0
- data/lib/ace/review/atoms/pr_comment_formatter.rb +22 -8
- data/lib/ace/review/molecules/campaign_evidence.rb +19 -10
- data/lib/ace/review/molecules/llm_executor.rb +5 -2
- data/lib/ace/review/molecules/pr_provider.rb +4 -1
- data/lib/ace/review/organisms/campaign_manager.rb +10 -1
- data/lib/ace/review/organisms/review_manager.rb +58 -13
- data/lib/ace/review/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: b74a181828465000d62fedc0062c118681144724a8beec940af5a335dade03b7
|
|
4
|
+
data.tar.gz: 700918d82fb6f27833a1b0f054301912d1a99dd5609b6140d93ff155a272f0b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3747d044dc834fe44d7977dfa120052709e5db45885a0c4fa39f7bbb60d4b819e6ea4c0f1f70c7189229b580f565b8936e6d5c1821ca04d4eb787a4cc84add61
|
|
7
|
+
data.tar.gz: 8ac3e30edd97356132095db4deca911ec62db8ba4c8ab0b92f7de8a11a8b6464cf5a9404180d8420d6eb18badb398ec158ee9e6f1b60a37f117d0dfbd4f6d6b6
|
|
@@ -60,6 +60,7 @@ instructions:
|
|
|
60
60
|
bundle: "project"
|
|
61
61
|
|
|
62
62
|
models:
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
# One reviewer position with fallback chain (roles in ace-llm config):
|
|
64
|
+
# pi glm 5.3 -> codex sol -> agy flash 3.8; the round is good if even
|
|
65
|
+
# one returns. astra runs once per PR via role:review-astra.
|
|
66
|
+
- role:review-default
|
|
@@ -35,6 +35,7 @@ instructions:
|
|
|
35
35
|
bundle: "project"
|
|
36
36
|
|
|
37
37
|
models:
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
# One reviewer position with fallback chain (roles in ace-llm config):
|
|
39
|
+
# pi glm 5.3 -> codex sol -> agy flash 3.8; the round is good if even
|
|
40
|
+
# one returns. astra runs once per PR via role:review-astra.
|
|
41
|
+
- role:review-default
|
data/CHANGELOG.md
CHANGED
|
@@ -7,15 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.58.1] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Campaign finish scopes availability re-hashing to the newest round's evidence and the content-addressed finding snapshots; earlier rounds validate through journal-backed historical authority reads, so resolving findings no longer permanently blocks finish.
|
|
14
|
+
|
|
15
|
+
|
|
10
16
|
## [0.58.0] - 2026-10-02
|
|
11
17
|
|
|
12
18
|
### Added
|
|
13
19
|
- Provider-neutral PR reviews and comment handling for GitHub, default Forgejo, and named Forgejo servers with exact-head evidence.
|
|
14
20
|
|
|
15
21
|
### Changed
|
|
22
|
+
- Review model queries honor the configured ace-llm fallback (role candidate
|
|
23
|
+
chains + `llm.fallback`) instead of forcing single-leg execution, so a
|
|
24
|
+
reviewer position survives a dead provider leg.
|
|
16
25
|
- Replaced `--gh-timeout` with `--provider-timeout`; removed the GitHub-only review fetcher, poster, and resolver paths.
|
|
17
26
|
- Preserve provider and repository identity across review sessions and comments; fail closed on moved heads and collection errors.
|
|
18
27
|
|
|
28
|
+
## [0.57.1] - 2026-10-02
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- A failed or incomplete provider entry no longer invalidates a campaign session's completed reviewer executions; one completed reviewer per needed scope still completes the round.
|
|
33
|
+
|
|
19
34
|
|
|
20
35
|
## [0.57.0] - 2026-10-01
|
|
21
36
|
|
|
@@ -42,18 +42,19 @@ module Ace
|
|
|
42
42
|
end
|
|
43
43
|
review_threads = comments_data[:review_threads] || []
|
|
44
44
|
|
|
45
|
-
# Build report sections
|
|
46
|
-
frontmatter = build_frontmatter(comments_data)
|
|
47
|
-
summary = build_summary(comments, reviews, review_threads, pr_number, pr_title)
|
|
48
45
|
# Apply the same bot filter to thread comments; threads left
|
|
49
|
-
# without human comments are omitted from feedback
|
|
46
|
+
# without human comments are omitted from feedback (filtered
|
|
47
|
+
# before the summary so bot-only threads never inflate counts).
|
|
50
48
|
review_threads = review_threads.map do |thread|
|
|
51
49
|
filtered = thread.merge(comments: Array(thread[:comments]).reject { |c| c[:author].to_s.match?(/\b(bot|github-actions|ace-review)\b/i) })
|
|
52
50
|
filtered.merge(comments: filtered[:comments])
|
|
53
51
|
end.reject { |thread| Array(thread[:comments]).empty? }
|
|
52
|
+
# Build report sections
|
|
53
|
+
frontmatter = build_frontmatter(comments_data)
|
|
54
|
+
summary = build_summary(comments, reviews, review_threads, pr_number, pr_title)
|
|
54
55
|
inline_section = build_inline_comments_section(review_threads)
|
|
55
56
|
unresolved_section = build_unresolved_section(comments, reviews)
|
|
56
|
-
resolved_section = build_resolved_section(reviews)
|
|
57
|
+
resolved_section = build_resolved_section(reviews, current_head: comments_data[:head_sha].to_s)
|
|
57
58
|
comments_table = build_comments_table(comments, reviews)
|
|
58
59
|
|
|
59
60
|
# Combine sections
|
|
@@ -190,13 +191,26 @@ module Ace
|
|
|
190
191
|
# Build resolved feedback section
|
|
191
192
|
#
|
|
192
193
|
# @param reviews [Array<Hash>] Code reviews
|
|
194
|
+
# @param current_head [String] Reviewed PR head (empty when unknown)
|
|
193
195
|
# @return [String, nil] Resolved section or nil if empty
|
|
194
|
-
def self.build_resolved_section(reviews)
|
|
196
|
+
def self.build_resolved_section(reviews, current_head: nil)
|
|
195
197
|
resolved = []
|
|
198
|
+
current = current_head.to_s
|
|
196
199
|
|
|
197
|
-
# Add approvals
|
|
200
|
+
# Add approvals. Approvals attach to a reviewed commit; only those
|
|
201
|
+
# matching the current head are current approvals, earlier-head
|
|
202
|
+
# approvals are labeled, and unknown-head approvals are called out
|
|
203
|
+
# as unknown instead of presenting as current.
|
|
198
204
|
reviews.select { |r| r[:state] == "APPROVED" }.each do |review|
|
|
199
|
-
|
|
205
|
+
review_head = review[:head_sha].to_s
|
|
206
|
+
label = if review_head.empty?
|
|
207
|
+
" (head unknown)"
|
|
208
|
+
elsif review_head == current
|
|
209
|
+
""
|
|
210
|
+
else
|
|
211
|
+
" (for earlier head #{review_head[0, 12]})"
|
|
212
|
+
end
|
|
213
|
+
resolved << "@#{review[:author]} approved changes#{label}"
|
|
200
214
|
end
|
|
201
215
|
|
|
202
216
|
return nil if resolved.empty?
|
|
@@ -18,6 +18,16 @@ 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
|
+
|
|
21
31
|
def initialize(repo_root:, check_evidence: nil, review_evidence: nil, approval_evidence: nil)
|
|
22
32
|
@repo_root = File.realpath(repo_root)
|
|
23
33
|
authority = CampaignExecutionEvidence.new(repo_root: @repo_root)
|
|
@@ -57,9 +67,9 @@ module Ace
|
|
|
57
67
|
metadata = json_keys(YAML.safe_load_file(metadata_path, permitted_classes: [Time, Date, Symbol]))
|
|
58
68
|
Contract.object!(metadata, "session metadata")
|
|
59
69
|
expected = {"campaign_id" => record["id"], "contract_identity" => record["contract_identity"],
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
"subject" => record["subject"], "round_id" => binding["round_id"], "scope" => scope,
|
|
71
|
+
"head" => binding["head"], "base" => binding["base"],
|
|
72
|
+
"scope_identity" => binding["scope_identity"][scope]}
|
|
63
73
|
unless metadata["campaign_binding"] == expected && metadata["preset"] == expected["scope_identity"]["preset"]
|
|
64
74
|
raise Contract::Invalid, "session campaign/contract/scope/head/base identity mismatch"
|
|
65
75
|
end
|
|
@@ -83,12 +93,7 @@ module Ace
|
|
|
83
93
|
reports = []
|
|
84
94
|
entries.each do |entry|
|
|
85
95
|
Contract.object!(entry, "model execution")
|
|
86
|
-
|
|
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
|
|
96
|
+
next unless self.class.completed_entry?(entry)
|
|
92
97
|
path = entry["output_file"].to_s
|
|
93
98
|
raise Contract::Invalid, "completed execution has no report" if path.empty?
|
|
94
99
|
path = File.expand_path(path, dir)
|
|
@@ -99,7 +104,7 @@ module Ace
|
|
|
99
104
|
resolved, report_ref = artifact("path" => path, "sha256" => entry["report_sha256"])
|
|
100
105
|
raise Contract::Invalid, "empty reviewer report" if File.read(resolved).strip.empty?
|
|
101
106
|
refs << report_ref
|
|
102
|
-
reports << {"artifact" => report_ref, "execution" => execution}
|
|
107
|
+
reports << {"artifact" => report_ref, "execution" => entry["execution"] || {}}
|
|
103
108
|
prompts = entry["prompt_sha256"]
|
|
104
109
|
Contract.object!(prompts, "execution prompt checksums")
|
|
105
110
|
%w[system user].each do |name|
|
|
@@ -117,6 +122,10 @@ module Ace
|
|
|
117
122
|
raise Contract::Invalid, "feedback extraction inventory or reviewed reports changed"
|
|
118
123
|
end
|
|
119
124
|
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?
|
|
120
129
|
completed &&= extracted
|
|
121
130
|
proof = completed ? @review_evidence.call(input.fetch("receipt"), head: binding["head"], artifacts: refs) : nil
|
|
122
131
|
{"receipt" => input["receipt"], "execution_proof" => proof, "path" => dir.delete_prefix(@repo_root + File::SEPARATOR), "scope" => scope,
|
|
@@ -113,8 +113,11 @@ module Ace
|
|
|
113
113
|
output: output_file,
|
|
114
114
|
format: "text",
|
|
115
115
|
timeout: timeout || Ace::Review.get("defaults", "llm_timeout") || 300,
|
|
116
|
-
force: true
|
|
117
|
-
|
|
116
|
+
force: true
|
|
117
|
+
# Fallback is config-driven (llm.fallback + role candidate
|
|
118
|
+
# chains): the reviewer position tries pi glm 5.3, then codex
|
|
119
|
+
# sol, then agy flash 3.8 — the round is good if even one
|
|
120
|
+
# returns, and the session records the winner only.
|
|
118
121
|
)
|
|
119
122
|
|
|
120
123
|
unless result[:text].is_a?(String) && !result[:text].strip.empty?
|
|
@@ -141,8 +141,11 @@ module Ace
|
|
|
141
141
|
comments: [{id: comment.id, author: comment.author, body: comment.body, url: comment.url}]}
|
|
142
142
|
end
|
|
143
143
|
reviews = snapshot.review_evidence.reviews.map do |review|
|
|
144
|
+
# Carry the reviewed commit so the formatter can tell
|
|
145
|
+
# current-head approvals from approvals of earlier heads.
|
|
144
146
|
{id: review.id, author: review.author, body: review.body,
|
|
145
|
-
state: review.state.to_s.upcase, url: review.url
|
|
147
|
+
state: review.state.to_s.upcase, url: review.url,
|
|
148
|
+
head_sha: review.head_sha}
|
|
146
149
|
end
|
|
147
150
|
{
|
|
148
151
|
success: true, comments: issue_comments, review_threads: threads,
|
|
@@ -310,7 +310,16 @@ module Ace
|
|
|
310
310
|
revision_error = e.message
|
|
311
311
|
end
|
|
312
312
|
round = record["rounds"].last
|
|
313
|
-
|
|
313
|
+
# Availability re-hashing covers the current round's evidence plus
|
|
314
|
+
# every finding snapshot. Earlier rounds' sessions and approvals are
|
|
315
|
+
# validated by journal-backed historical authority reads instead:
|
|
316
|
+
# their working files legitimately age (feedback resolve archives
|
|
317
|
+
# and annotates finding files; test report `latest` paths move on),
|
|
318
|
+
# so hashing them here would permanently block finish for any
|
|
319
|
+
# campaign that resolved findings. Finding snapshots are
|
|
320
|
+
# content-addressed and must remain available.
|
|
321
|
+
current_attempt_ids = round ? [round["attempt_id"]] : []
|
|
322
|
+
refs = record["attempts"].select { |attempt| current_attempt_ids.include?(attempt["attempt_id"]) }.flat_map do |attempt|
|
|
314
323
|
attempt["sessions"].flat_map { |session| session["artifacts"] } +
|
|
315
324
|
(attempt["approval"] ? [attempt["approval"]["artifact"]] + attempt["approval"]["artifacts"] : [])
|
|
316
325
|
end + (record["assessments"] + record["inherited_findings"]).map { |finding| finding["artifact"] }
|
|
@@ -376,6 +376,18 @@ module Ace
|
|
|
376
376
|
delta = Molecules::DeltaResolver.resolve(options.delta, metadata, project_root: @project_root || Dir.pwd)
|
|
377
377
|
return {success: false, error: delta[:error]} unless delta[:success]
|
|
378
378
|
|
|
379
|
+
# The snapshot above was read before resolution ran; re-read the
|
|
380
|
+
# provider metadata and compare identity so a head, base, or
|
|
381
|
+
# repository move during delta computation fails the round instead
|
|
382
|
+
# of certifying a delta against stale identity.
|
|
383
|
+
recheck_result = pr_provider(options).fetch_metadata(pr_identifier)
|
|
384
|
+
return {success: false, error: recheck_result[:error]} unless recheck_result[:success]
|
|
385
|
+
rechecked = recheck_result[:metadata]
|
|
386
|
+
identity_keys = %w[server_name provider repository_url number headRefOid baseRefOid]
|
|
387
|
+
if identity_keys.any? { |key| rechecked[key] != metadata[key] }
|
|
388
|
+
return {success: false, error: "PR identity changed during delta resolution; delta round rejected"}
|
|
389
|
+
end
|
|
390
|
+
|
|
379
391
|
options.pr_metadata = metadata
|
|
380
392
|
# Carry the reference session's findings forward as evidence; an explicit
|
|
381
393
|
# --evidence-session still merges on top.
|
|
@@ -413,7 +425,8 @@ module Ace
|
|
|
413
425
|
pr_metadata: metadata, delta: delta,
|
|
414
426
|
diff_manifest: manifest.merge(scoped[:manifest]).merge(
|
|
415
427
|
exempt_files: classification[:exempt],
|
|
416
|
-
exempt_patterns: Array(config[:exempt_paths])
|
|
428
|
+
exempt_patterns: Array(config[:exempt_paths])
|
|
429
|
+
)}
|
|
417
430
|
end
|
|
418
431
|
|
|
419
432
|
scoped = apply_exempt_paths(scoped, config)
|
|
@@ -494,10 +507,10 @@ module Ace
|
|
|
494
507
|
lines << ""
|
|
495
508
|
lines << "# No-op review round"
|
|
496
509
|
lines << ""
|
|
497
|
-
if content[:noop_reason] == :empty_delta && delta
|
|
498
|
-
|
|
510
|
+
lines << if content[:noop_reason] == :empty_delta && delta
|
|
511
|
+
"Scope: delta since `#{delta[:reference_head]}` — the delta is empty; there is no change to review."
|
|
499
512
|
else
|
|
500
|
-
|
|
513
|
+
"Scope: #{content[:noop_scope] || "no-op"}."
|
|
501
514
|
end
|
|
502
515
|
lines << ""
|
|
503
516
|
lines << "Verdict: no new findings. This round is clean only if no carried-forward Critical/High finding remains unresolved."
|
|
@@ -1395,13 +1408,21 @@ module Ace
|
|
|
1395
1408
|
# Read review content
|
|
1396
1409
|
review_content = File.read(review_file)
|
|
1397
1410
|
|
|
1411
|
+
# The identity digest covers only stable review content: the
|
|
1412
|
+
# metadata header carries a fresh timestamp on every run, so hashing
|
|
1413
|
+
# the whole file would mint a new identity for identical review
|
|
1414
|
+
# text and let an uncertain-post retry post a duplicate. The
|
|
1415
|
+
# dry-run preview uses the same digest so preview and post agree.
|
|
1416
|
+
stable_content = review_content.sub(/\A---\n.*?\n---\n\n/m, "")
|
|
1417
|
+
review_digest = Digest::SHA256.hexdigest(stable_content)
|
|
1418
|
+
|
|
1398
1419
|
# Dry-run prepares the exact comment body without posting; the
|
|
1399
1420
|
# response builder reads :preview. The timestamp derives from the
|
|
1400
1421
|
# review digest — identical to what a real post would send.
|
|
1401
1422
|
if options.dry_run
|
|
1402
1423
|
preview = Molecules::PrProvider.format_comment(
|
|
1403
1424
|
review_content, preset: review_data[:preset], model: review_data[:model],
|
|
1404
|
-
timestamp: "review-#{
|
|
1425
|
+
timestamp: "review-#{review_digest[0, 12]}"
|
|
1405
1426
|
)
|
|
1406
1427
|
return {success: true, dry_run: true, preview: preview}
|
|
1407
1428
|
end
|
|
@@ -1416,7 +1437,6 @@ module Ace
|
|
|
1416
1437
|
# keyed by everything reuse depends on: PR identity, head, and
|
|
1417
1438
|
# the review artifact digest. Identical review text for another
|
|
1418
1439
|
# PR therefore never collides.
|
|
1419
|
-
review_digest = Digest::SHA256.file(review_file).hexdigest
|
|
1420
1440
|
# The numeric PR (not the raw reference spelling) keys the
|
|
1421
1441
|
# persisted identity, matching the provider-side marker basis.
|
|
1422
1442
|
identity_pr = (options.pr_metadata&.fetch("number", nil) || options.pr).to_s
|
|
@@ -1429,6 +1449,10 @@ module Ace
|
|
|
1429
1449
|
# any new timestamp is generated.
|
|
1430
1450
|
identity_preset = review_data[:preset].to_s
|
|
1431
1451
|
identity_model = review_data[:model].to_s
|
|
1452
|
+
# The identity includes the review artifact digest: a retry of
|
|
1453
|
+
# the same artifact reuses the persisted marker (no duplicate),
|
|
1454
|
+
# while a regenerated review for a later round is a new artifact
|
|
1455
|
+
# and gets its own comment.
|
|
1432
1456
|
identity_key = Digest::SHA256.hexdigest(
|
|
1433
1457
|
[identity_server, identity_repo, identity_pr, identity_head, review_digest,
|
|
1434
1458
|
identity_preset, identity_model].join("\0")
|
|
@@ -1454,10 +1478,13 @@ module Ace
|
|
|
1454
1478
|
else
|
|
1455
1479
|
# The marker IS the identity key: deterministic for the same
|
|
1456
1480
|
# identity even when the local record was lost, so the posted
|
|
1457
|
-
# body and correlation always reconcile.
|
|
1481
|
+
# body and correlation always reconcile. The body is formatted
|
|
1482
|
+
# from the same stable content the digest hashes: embedding the
|
|
1483
|
+
# timestamped frontmatter would make a rerun with identical
|
|
1484
|
+
# review text report a content conflict after a lost record.
|
|
1458
1485
|
session_key = identity_key
|
|
1459
1486
|
content = Molecules::PrProvider.format_comment(
|
|
1460
|
-
|
|
1487
|
+
stable_content, preset: review_data[:preset], model: review_data[:model],
|
|
1461
1488
|
# Deterministic per identity: the review digest stands in
|
|
1462
1489
|
# for the mtime so re-formatting after a lost record still
|
|
1463
1490
|
# reconciles.
|
|
@@ -1496,8 +1523,12 @@ module Ace
|
|
|
1496
1523
|
reviewed_server = options.pr_metadata&.fetch("server_name", nil)
|
|
1497
1524
|
reviewed_repo = options.pr_metadata&.fetch("repository_url", nil)
|
|
1498
1525
|
reviewed_number = options.pr_metadata&.fetch("number", nil)
|
|
1526
|
+
# Normalize both sides: forgejo strips .git from its PR URL while
|
|
1527
|
+
# comment evidence retains the configured URL, so exact comparison
|
|
1528
|
+
# would fail a successful post.
|
|
1529
|
+
normalize = ->(url) { url.to_s.chomp("/").sub(/\.git\z/i, "") }
|
|
1499
1530
|
if (reviewed_server && receipt.comment.server_name != reviewed_server) ||
|
|
1500
|
-
(reviewed_repo && receipt.comment.repository_url != reviewed_repo) ||
|
|
1531
|
+
(reviewed_repo && normalize.call(receipt.comment.repository_url) != normalize.call(reviewed_repo)) ||
|
|
1501
1532
|
(reviewed_number && receipt.comment.pr_number != reviewed_number)
|
|
1502
1533
|
raise Ace::Git::ProviderIdentityMismatchError,
|
|
1503
1534
|
"Posted comment identity (#{receipt.comment.server_name}/#{receipt.comment.repository_url}##{receipt.comment.pr_number}) does not match the reviewed PR"
|
|
@@ -1757,7 +1788,17 @@ module Ace
|
|
|
1757
1788
|
# completes; an absent directory cannot attest a zero-findings review.
|
|
1758
1789
|
def save_campaign_feedback_metadata(session_dir, result)
|
|
1759
1790
|
path = File.join(session_dir, "metadata.yml")
|
|
1760
|
-
metadata
|
|
1791
|
+
# Session metadata is symbol-keyed YAML; normalize before any key
|
|
1792
|
+
# reads so the completed-execution rule and the binding check see
|
|
1793
|
+
# string keys, matching the campaign evidence reader.
|
|
1794
|
+
normalize = lambda do |value|
|
|
1795
|
+
case value
|
|
1796
|
+
when Hash then value.to_h { |k, v| [k.to_s, normalize.call(v)] }
|
|
1797
|
+
when Array then value.map { |v| normalize.call(v) }
|
|
1798
|
+
else value
|
|
1799
|
+
end
|
|
1800
|
+
end
|
|
1801
|
+
metadata = normalize.call(YAML.safe_load_file(path, permitted_classes: [Time, Date, Symbol]))
|
|
1761
1802
|
return unless metadata["campaign_binding"]
|
|
1762
1803
|
extraction = {"status" => result ? "failed" : "skipped"}
|
|
1763
1804
|
if result && result[:success]
|
|
@@ -1770,9 +1811,13 @@ module Ace
|
|
|
1770
1811
|
unless result[:items_count] == ids.size && ids.uniq.size == ids.size
|
|
1771
1812
|
raise Errors::BundleProcessingError, "feedback extraction inventory is incomplete"
|
|
1772
1813
|
end
|
|
1773
|
-
entries = metadata["models"] || [YAML.safe_load_file(
|
|
1774
|
-
permitted_classes: [Time, Date, Symbol]
|
|
1775
|
-
|
|
1814
|
+
entries = metadata["models"] || [normalize.call(YAML.safe_load_file(
|
|
1815
|
+
File.join(session_dir, "llm_metadata.yml"), permitted_classes: [Time, Date, Symbol]
|
|
1816
|
+
))]
|
|
1817
|
+
# Same completed rule as the campaign evidence reader: a failed
|
|
1818
|
+
# provider that left partial output is not a reviewed report.
|
|
1819
|
+
digests = entries.select { |entry| Molecules::CampaignEvidence.completed_entry?(entry) }
|
|
1820
|
+
.map { |entry| entry["report_sha256"] }.compact
|
|
1776
1821
|
extraction = {"status" => "succeeded", "finding_ids" => ids, "report_sha256" => digests}
|
|
1777
1822
|
end
|
|
1778
1823
|
metadata["feedback_extraction"] = extraction
|
data/lib/ace/review/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-review
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.58.
|
|
4
|
+
version: 0.58.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|