ace-review 0.59.0 → 0.60.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/config.yml +8 -2
- data/CHANGELOG.md +32 -0
- data/docs/campaigns.md +7 -2
- data/docs/usage.md +38 -1
- data/handbook/workflow-instructions/review/pr.wf.md +77 -1
- data/lib/ace/review/atoms/campaign_contract.rb +21 -9
- data/lib/ace/review/atoms/campaign_policy.rb +184 -0
- data/lib/ace/review/atoms/campaign_projection.rb +16 -19
- data/lib/ace/review/cli/campaign_cli.rb +6 -2
- data/lib/ace/review/cli/commands/campaign/assess.rb +22 -0
- data/lib/ace/review/cli/commands/campaign/finish.rb +4 -2
- data/lib/ace/review/cli/commands/campaign/resume.rb +28 -0
- data/lib/ace/review/cli/commands/campaign/status.rb +3 -1
- data/lib/ace/review/cli/commands/feedback/create.rb +1 -0
- data/lib/ace/review/cli/commands/review.rb +1 -0
- data/lib/ace/review/molecules/campaign_evidence.rb +58 -15
- data/lib/ace/review/molecules/campaign_execution_evidence.rb +2 -2
- data/lib/ace/review/molecules/campaign_store.rb +52 -3
- data/lib/ace/review/molecules/feedback_synthesizer.rb +32 -19
- data/lib/ace/review/molecules/llm_executor.rb +54 -6
- data/lib/ace/review/molecules/multi_model_executor.rb +3 -1
- data/lib/ace/review/molecules/subject_extractor.rb +4 -3
- data/lib/ace/review/organisms/campaign_manager.rb +396 -20
- data/lib/ace/review/organisms/feedback_manager.rb +2 -1
- data/lib/ace/review/organisms/prepared_candidate_review.rb +146 -0
- data/lib/ace/review/organisms/review_manager.rb +91 -26
- data/lib/ace/review/version.rb +1 -1
- metadata +32 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acf36b5361402d845cc259ac24c06b4dd1e686cbc25b0a80b62c34ae8453ab53
|
|
4
|
+
data.tar.gz: 1fbd838c775362c85876d5c160e5352ba3f048a502034659f3f3d14429ae43f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: adc5171f507ca5b4adc1ada99d0688fb2878236a21339ef7d1fd6c5341e017966a3c721e09cb5bb33a528ebc239e261b137501e9db49fedea4ac9bb100b0cc6f
|
|
7
|
+
data.tar.gz: 2c7751781253a309563b56d204a981299429f02de6ce09313b19f6cd652e0c4d9f6d68ed83647460778ed6a3edc3e8d25e65995f09650964e754c5995aa251b4
|
|
@@ -10,7 +10,6 @@
|
|
|
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
|
-
# gh_simple_timeout - Timeout for simple gh commands like --version, auth status (default: 10)
|
|
14
13
|
# llm_timeout - Timeout for LLM queries in seconds (default: 900)
|
|
15
14
|
#
|
|
16
15
|
# Environment Variables:
|
|
@@ -38,7 +37,6 @@ defaults:
|
|
|
38
37
|
max_concurrent_models: 3 # Max parallel LLM queries for multi-model execution
|
|
39
38
|
auto_execute: false # Set to true to auto-execute LLM query without confirmation
|
|
40
39
|
provider_timeout: 30 # Timeout for forge operations in seconds
|
|
41
|
-
gh_simple_timeout: 10 # Timeout for simple gh commands (--version, auth status)
|
|
42
40
|
llm_timeout: 900 # Timeout for LLM queries in seconds (15 minutes)
|
|
43
41
|
|
|
44
42
|
# Review-exempt paths
|
|
@@ -79,5 +77,13 @@ campaign:
|
|
|
79
77
|
revision: delivery-v1
|
|
80
78
|
minimum_rounds: 3
|
|
81
79
|
clean_rounds: 2
|
|
80
|
+
maximum_rounds: 5
|
|
81
|
+
required_scopes: [full]
|
|
82
|
+
required_checks: [tests]
|
|
83
|
+
discovery:
|
|
84
|
+
revision: discovery-v1
|
|
85
|
+
minimum_rounds: 2
|
|
86
|
+
clean_rounds: 1
|
|
87
|
+
maximum_rounds: 2
|
|
82
88
|
required_scopes: [full]
|
|
83
89
|
required_checks: [tests]
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.60.1] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Require the published producer and consumer patch versions containing the protected context and managed-attempt APIs; exclude incompatible prior dependency graphs.
|
|
15
|
+
|
|
16
|
+
## [0.60.0] - 2026-10-08
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Enforce finite campaign phases, durable provider-attempt reservations, convergence limits, escalation and resume through the same campaign owner.
|
|
21
|
+
- Join canonical collection/check/approval child receipts and current parent result acceptance.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Accept forge-neutral URLs in typed PR subjects and use the verified held result snapshot without a second projection.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- ReviewManager can execute a bounded prepared candidate through the maintained provider and feedback pipeline, requiring an explicit bound verdict and complete execution evidence before returning approval.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Remove the unused GitHub CLI timeout setting from the shipped defaults; forge operations use provider_timeout.
|
|
34
|
+
- Reject missing, duplicate-key or malformed feedback inventories instead of silently dropping findings and reporting a clean extraction.
|
|
35
|
+
|
|
36
|
+
## [0.59.1] - 2026-10-06
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Concurrent review starts exclusively claim isolated sessions, refuse occupied explicit paths, and preserve report and feedback provenance. Single-model execution reports its exact session path; feedback creation discovers its `review.md`. Release exports publish complete copies without replacing equal-clock reports.
|
|
41
|
+
|
|
10
42
|
## [0.59.0] - 2026-10-05
|
|
11
43
|
|
|
12
44
|
### Changed
|
data/docs/campaigns.md
CHANGED
|
@@ -103,13 +103,14 @@ The final round may reference an explicit approval artifact with `approval: {"pa
|
|
|
103
103
|
```json
|
|
104
104
|
{
|
|
105
105
|
"producer":"IMPLEMENTER_ACTOR",
|
|
106
|
-
"reviewer":"
|
|
106
|
+
"reviewer":"INDEPENDENT_REVIEWER_ACTOR",
|
|
107
107
|
"verdict":"approved",
|
|
108
108
|
"head":"EXACT_REVIEWED_HEAD_SHA",
|
|
109
109
|
"base":"EXACT_REVIEWED_BASE_SHA",
|
|
110
110
|
"contract_identity":"FROZEN_CONTRACT_SHA256",
|
|
111
111
|
"required_scopes":["full"],
|
|
112
112
|
"reports":[{"path":"SESSION/review-report-MODEL.md","sha256":"REPORT_SHA256"}],
|
|
113
|
+
"report_models":[{"report":{"path":"SESSION/review-report-MODEL.md","sha256":"REPORT_SHA256"},"report_model":"ACTUAL_EXECUTED_MODEL"}],
|
|
113
114
|
"receipt":{"attempt_id":"ACCEPTED_REVIEW_APPROVAL_ATTEMPT_ID","digest":"ACCEPTED_RECEIPT_SHA256"},
|
|
114
115
|
"checks":[{"name":"tests","verdict":"passed","receipt":{
|
|
115
116
|
"attempt_id":"ACCEPTED_CHECK_ATTEMPT_ID","digest":"ACCEPTED_RECEIPT_SHA256"
|
|
@@ -119,7 +120,7 @@ The final round may reference an explicit approval artifact with `approval: {"pa
|
|
|
119
120
|
|
|
120
121
|
The approval `receipt` refers to an ordinary managed `operation: "review"` receipt with an executed `review.verdict: "approved"`, the same exact head, producer/reviewer actors and checksummed report artifacts. Submit that attributable outcome through the existing coordinator after the independent reviewer actually approves. Do not include a campaign result in this source receipt; campaign consumption happens afterward. The read-only `attempt evidence --kind review-approval` boundary revalidates its accepted authority. A collection receipt certifies execution even when its report rejects the candidate; it cannot certify approval. Every retained approval remains authority-backed across head changes through explicit historical validation.
|
|
121
122
|
|
|
122
|
-
Producer and reviewer must differ. Reviewer
|
|
123
|
+
Producer and reviewer must differ. Reviewer identifies the authenticated actor, independently of the model. Each report must have exactly one `report_models` entry whose checksummed reference belongs to `reports` and whose `report_model` matches verified session execution metadata. Missing, duplicate, extra or mismatched entries fail; model names do not substitute for actor attribution. Approvals must cover every required scope. Required checks refer to a succeeded execution receipt already accepted by the ace-assign coordinator. The receipt operation must match the check: `tests` requires `test`; other explicit check names require the same operation name. An unrelated operation claiming a passed tests field is rejected. Run the check under an existing managed assignment attempt and submit its attributable result, current head, check outcomes and checksummed artifacts through `ace-assign attempt finish`. Use the accepted attempt ID and receipt digest here. A self-authored check JSON file is insufficient. Campaign recording and acceptance consult the read-only `ace-assign attempt evidence --attempt ID --receipt-digest DIGEST --format json --kind check --check-name tests` boundary, which revalidates current head and source artifacts against accepted history. Retain `ace-assign` alongside `ace-review` for this qjl evidence capability; unavailable authority blocks acceptance explicitly. Campaign commands never run the check for you or create a second execution journal. Authenticated producer/reviewer attribution and execution acceptance remain owned by ace-assign.
|
|
123
124
|
|
|
124
125
|
```sh
|
|
125
126
|
ace-review campaign finish CAMPAIGN_ID --format json
|
|
@@ -139,6 +140,10 @@ Submit a normal succeeded review receipt through `ace-assign attempt finish`. Ad
|
|
|
139
140
|
|
|
140
141
|
The receipt retains ordinary attempt/assignment/project/scope binding, producer, current head, reviewer approval and executed checks. Its producer/reviewer actors must match the campaign assessment. The trusted coordinator rechecks live campaign state and rejects stale, blocked, mismatched or dry-run results. Accepting evidence records a separate journal commit without moving the candidate branch.
|
|
141
142
|
|
|
143
|
+
## Guarded source consumption
|
|
144
|
+
|
|
145
|
+
Source-owned receipt composition can call `CampaignManager#with_verified_result!(result:, subject:, contract_identity:, policy:, head:, base:, producer:, reviewer:) { |current| ... }`. It requires a block and an existing campaign lock, revalidates current acceptance and the exact expected tuple, and yields a deeply frozen projection while holding one read transaction. Keep the entire receipt/CAS operation inside that block; a retained projection is not a later permission. Do not call public campaign methods from inside the block. Exceptions release the lock, and validation failures never yield. This API does not itself provide protected child linkage or authorize a journal mutation.
|
|
146
|
+
|
|
142
147
|
## Restart, contract changes and failures
|
|
143
148
|
|
|
144
149
|
Campaign authority lives in `.ace-local/review/campaigns/`; retain this directory and source evidence across process/context restarts. Records are serialized with a lock and replaced atomically with checksums. Every retained completed session, including earlier rounds and partial coverage, must keep its immutable accepted review-collection authority. Historical reads use `ace-assign attempt evidence --kind review-collection --historical-head EXACT_RECORDED_HEAD` to validate that receipt and its artifacts. This read-only mode cannot certify current checks; check-purpose proofs always require the live head. Losing/corrupting evidence from any retained counted round or assessment makes it unavailable; historical counters remain diagnostic and do not manufacture acceptance. These records are durable local artifacts, not a second assignment execution journal.
|
data/docs/usage.md
CHANGED
|
@@ -65,7 +65,36 @@ ace-review [OPTIONS]
|
|
|
65
65
|
| `--no-feedback` | Skip feedback extraction |
|
|
66
66
|
| `--feedback-model` | Model for feedback extraction |
|
|
67
67
|
| `--save-session` | Save session files (default: true) |
|
|
68
|
-
| `--session-dir` |
|
|
68
|
+
| `--session-dir` | Fresh session directory; existing empty directories may be claimed once |
|
|
69
|
+
|
|
70
|
+
### Concurrent reviews and exact session selection
|
|
71
|
+
|
|
72
|
+
Each new review claims its own session before writing prompts or metadata. Automatic
|
|
73
|
+
sessions use `.ace-local/review/sessions/review-<id>` with an opaque suffix when a
|
|
74
|
+
name collides. Successful execution prints both the exported report and the actual
|
|
75
|
+
session directory. Same-clock reviews preserve separate reports and feedback.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bin/ace-review --preset code-valid --subject diff:BASE..HEAD --auto-execute
|
|
79
|
+
# Output includes: Session directory: /project/.ace-local/review/sessions/review-<id>
|
|
80
|
+
bin/ace-review-feedback list --session /project/.ace-local/review/sessions/review-<id>
|
|
81
|
+
# Output contains only this session's findings.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For manual feedback creation, pass the exact reported session path to
|
|
85
|
+
`bin/ace-review-feedback create --session PATH`. Single-model `review.md` and
|
|
86
|
+
multi-model reports are discovered within that directory. Published findings are
|
|
87
|
+
immutable; start a fresh review when findings already exist.
|
|
88
|
+
|
|
89
|
+
An explicit path must be absent or an unclaimed empty directory. A permanent claim
|
|
90
|
+
remains after success, failure or interruption. Reusing a claimed or nonempty path,
|
|
91
|
+
a regular file, or a symlink fails before reviewer execution and preserves existing
|
|
92
|
+
contents. Choose a fresh writable path after an allocation refusal:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bin/ace-review --preset code-valid --subject diff:BASE..HEAD --session-dir .ace-local/review/source-round-2
|
|
96
|
+
# Output includes: Review session prepared: .ace-local/review/source-round-2
|
|
97
|
+
```
|
|
69
98
|
|
|
70
99
|
### Informational
|
|
71
100
|
|
|
@@ -117,6 +146,14 @@ ace-review --list-prompts
|
|
|
117
146
|
|
|
118
147
|
Manage feedback items extracted from reviews.
|
|
119
148
|
|
|
149
|
+
Extraction requires an explicit `findings` array in the synthesis response.
|
|
150
|
+
A missing array, duplicate JSON key, or finding without a nonempty textual title
|
|
151
|
+
and description fails the entire extraction; valid findings are not published
|
|
152
|
+
as a partial replacement. For example, `{"findings":[{"title":"","finding":"Bug"}]}`
|
|
153
|
+
is an extraction failure, while `{"findings":[]}` is a completed empty extraction.
|
|
154
|
+
Neither a provider's successful exit nor empty extraction is an approval verdict.
|
|
155
|
+
|
|
156
|
+
|
|
120
157
|
### Subcommands
|
|
121
158
|
|
|
122
159
|
| Command | Description |
|
|
@@ -17,6 +17,17 @@ One round covers the PR's needed scope, including cross-module integration. A la
|
|
|
17
17
|
|
|
18
18
|
Use at least one available reviewer per needed scope. Prefer the project's configured models, but preferences are not an allowlist. Honor the user's current provider/model restrictions. Record actual provider, model, reasoning and completion status. A zero process exit code alone is not a completed review.
|
|
19
19
|
|
|
20
|
+
## Execution boundary
|
|
21
|
+
|
|
22
|
+
The ordinary commands below use the local campaign store and coordinator-accepted
|
|
23
|
+
ordinary receipts. For an installed protected parent with registered
|
|
24
|
+
`review_campaign`, use its original authenticated authority and the protected
|
|
25
|
+
handoff below instead. The installed launcher owns the original linked collection,
|
|
26
|
+
check and approval children; each must succeed, settle and release before its
|
|
27
|
+
receipt can count. A worker-local report or ordinary `attempt finish` cannot
|
|
28
|
+
replace that linkage. Draft creation/update may precede review; ready and merge
|
|
29
|
+
still require the accepted current-candidate result and executed checks.
|
|
30
|
+
|
|
20
31
|
## 1. Choose and prepare the scope
|
|
21
32
|
|
|
22
33
|
Identify the PR from an explicit number, owner/repository#number or URL and read normalized evidence with `ace-git pr show IDENTIFIER --format json`. Use the assignment's retained forge selection: `--server NAME` or `--default-server`, mutually exclusive; otherwise resolve the repository remote. Keep that same selection on `ace-review --pr IDENTIFIER` in every round. Do not infer a forge or PR from a fixed hostname or caller identity. Read its goal, current requirements, relevant ADRs and changed-file list. Use the smallest set of presets covering the change. Small coherent PRs can be reviewed whole; split large inputs by high-level module or functional lens, not arbitrary file chunks. Include needed contracts and code when reviewing tests and include integration behavior when modules interact.
|
|
@@ -37,7 +48,7 @@ Verify reports against code and requirements. Use `ace-review-feedback list --se
|
|
|
37
48
|
|
|
38
49
|
Fix confirmed P0/P1 and run appropriate tests. Verify P2 and lower findings, fix those within the task or defer with rationale; they do not independently require another round. Do not expand the PR to implement speculative improvements. If the same blocker recurs without an effective fix, diagnose the cause or request the needed product/architecture decision instead of repeating the same review.
|
|
39
50
|
|
|
40
|
-
## 3.
|
|
51
|
+
## 3. Ordinary campaign recording and finish
|
|
41
52
|
|
|
42
53
|
Use the durable `ace-review campaign` authority for completed rounds, verified dispositions, required scopes and clean streak. Start/reuse it with explicit subject JSON and a frozen behavioral requirements document; snapshot the applicable delivery policy. Pin an empty round before collection with head/base, scope IDs and presets, then pass `--campaign ID --campaign-round ROUND --campaign-scope SCOPE` to the existing review runner. Have the existing assignment coordinator accept each completed collection as a `review-collect` receipt with a passed review-execution outcome and checksummed metadata/report/prompt artifacts. Include its accepted receipt reference with the session and verified feedback dispositions in `campaign record-round`; editable execution flags alone cannot count. Full PR scopes require the complete unfiltered whole-PR inventory without delta references. Filtered module reviews and delta reviews use explicit module or delta scopes frozen in the campaign policy; pin the delta reference before collection. The default delivery `full` policy requires whole-PR rounds. Record partial attempts without counting them as completed rounds. See `ace-review/docs/campaigns.md` for exact input schemas and examples.
|
|
43
54
|
|
|
@@ -52,3 +63,68 @@ Examples:
|
|
|
52
63
|
- A round that finds a confirmed P1 is not clean even if it is fixed immediately afterward.
|
|
53
64
|
|
|
54
65
|
After the completion rule is met and changes have appropriate test results, update the PR description with actual reviewers, scopes, fixes, deferred findings and limitations, then mark the draft ready. Finish with `ace-review campaign finish ID --format json` after the final round records independent current-head approval backed by an accepted ordinary review receipt and passing required check artifacts. The source approval receipt must bind the approved reviewer verdict, actors, head and report hashes; caller-written approval JSON and collection receipts alone cannot approve the candidate. Search convergence alone cannot approve stale/incomplete evidence or unresolved earlier P0/P1. The result remains a local artifact until ace-assign independently verifies it through its existing attempt receipt boundary. Keep formal merge checks at merge time. Do not merge without explicit user authorization, post replies to people or resolve their threads without authorization. Do not make optional code changes after completion that would unnecessarily reopen review.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Protected campaign recording and parent result handoff
|
|
69
|
+
|
|
70
|
+
Use the original parent mapping, assignment, attempt, exact candidate SHA and
|
|
71
|
+
positive candidate generation from its accepted registration/status. Campaign ID,
|
|
72
|
+
subject, frozen requirements identity and policy come from that registered parent;
|
|
73
|
+
these commands have no caller-selected campaign, project, scope or base flags.
|
|
74
|
+
The round's base and scopes remain the exact pinned R1 input. Do not run local
|
|
75
|
+
`ace-review campaign record-round` or `finish` as an installed authority substitute.
|
|
76
|
+
|
|
77
|
+
Upload the existing R1 round JSON inside the closed R2 envelope:
|
|
78
|
+
`{"version":1,"round":ROUND_OBJECT,"artifacts":[{"path":"report.md","sha256":"SHA256"}]}`.
|
|
79
|
+
`ROUND_OBJECT` retains the documented attempt_id, round_id, head/base,
|
|
80
|
+
required_scopes, scope_identity, sessions, dispositions and optional approval.
|
|
81
|
+
Its attempt_id equals the stable mutation ID below and its head equals the parent
|
|
82
|
+
SHA. Each artifact path is a bounded relative materialization name; it is not an
|
|
83
|
+
instruction for the authority to read the worker filesystem. Supply exact report,
|
|
84
|
+
metadata, prompt, feedback and approval bytes referenced by that round as ordered
|
|
85
|
+
`--artifact` files matching the envelope's paths/digests. The envelope is at most
|
|
86
|
+
16 KiB, with at most 16 artifacts of at most 64 KiB each. The authority privately
|
|
87
|
+
materializes bytes and resolves child receipt references from canonical history;
|
|
88
|
+
collection proves execution, while independently accepted approval and matching
|
|
89
|
+
check children establish acceptance. Never add the parent campaign result to a
|
|
90
|
+
source child receipt.
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
ace-assign campaign-record-round --mapping MAPPING --assignment PARENT --attempt ATTEMPT --head SHA --candidate-generation N --mutation ROUND-ATTEMPT --input ROUND-ENVELOPE.json --artifact REPORT-BYTES
|
|
94
|
+
ace-assign campaign-export-result --mapping MAPPING --assignment PARENT --attempt ATTEMPT --head SHA --candidate-generation N --output RESULT.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Repeat `--artifact` for all envelope artifacts in declared order, or omit it for
|
|
98
|
+
an empty pin. Record each completed round under the frozen policy; export succeeds
|
|
99
|
+
only when that policy, current candidate, independent approval and required checks
|
|
100
|
+
are accepted. Export creates mode0600 result bytes once and reports path, sha256,
|
|
101
|
+
result_identity and journal_commit. An existing output is reusable only when its
|
|
102
|
+
bytes match exactly; do not rewrite or compose a replacement result.
|
|
103
|
+
|
|
104
|
+
The original parent receipt keeps its existing assignment/attempt/project/scope,
|
|
105
|
+
producer, exact head, `operation: "review"`, `verdict: "succeeded"`, independent
|
|
106
|
+
reviewer approval and executed checks. Add
|
|
107
|
+
`"campaign":{"id":"REGISTERED-CAMPAIGN","result":{"path":"RESULT.json","sha256":"EXPORTED-SHA256"}}`
|
|
108
|
+
and include that identical reference in its `artifacts` array. Supply the exact
|
|
109
|
+
exported bytes in the matching ordered upload slot alongside the receipt's other
|
|
110
|
+
artifacts; local artifact names become canonical imported references on acceptance.
|
|
111
|
+
Submit through the maintained public boundary, using the persisted authority
|
|
112
|
+
generation and a stable original result mutation:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
ace-assign submit-result --mapping MAPPING --assignment PARENT --attempt ATTEMPT --head SHA --candidate-generation N --expected-generation G --mutation RESULT-MUTATION --receipt PARENT-RECEIPT.json --artifact RESULT.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This submits evidence; it does not independently approve, finish or release the
|
|
119
|
+
parent. The original reviewer/finish owner retains that gate. Authority generation
|
|
120
|
+
G and candidate generation N are distinct. Ready/merge revalidate this canonical
|
|
121
|
+
parent campaign result and checks at the current candidate.
|
|
122
|
+
|
|
123
|
+
On a lost reply/restart, preserve the exact round mutation, envelope and ordered
|
|
124
|
+
bytes; identical round replay returns retained history without another completed
|
|
125
|
+
round. Re-export may only reuse identical result bytes. Preserve original result
|
|
126
|
+
mutation, generation and upload bytes for result replay; do not refresh selectors
|
|
127
|
+
to manufacture success. Changed candidate, policy, requirements or missing child
|
|
128
|
+
settlement/source artifacts refuses current export/submission/ready/merge while
|
|
129
|
+
retaining historical rounds. Read original canonical status and route the precise
|
|
130
|
+
producer blocker; do not fabricate receipts or bypass to ordinary commands.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "ace/git/atoms/server_url"
|
|
5
7
|
|
|
6
8
|
module Ace
|
|
7
9
|
module Review
|
|
@@ -86,15 +88,22 @@ module Ace
|
|
|
86
88
|
parsed = Ace::Git::Atoms::PrIdentifier.parse(string!(pr, "pr"))
|
|
87
89
|
raise Invalid, "PR must include repository identity (owner/repo#number)" unless parsed&.repo
|
|
88
90
|
raise Invalid, "PR number must be positive" unless parsed.number.to_i.positive?
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
raise Invalid, "PR repository identity does not match the declared repository"
|
|
94
|
-
end
|
|
95
|
-
repo = repo.downcase
|
|
96
|
-
repository = "https://github.com/#{repo}"
|
|
91
|
+
begin
|
|
92
|
+
uri = URI.parse(repository)
|
|
93
|
+
rescue URI::InvalidURIError
|
|
94
|
+
raise Invalid, "PR repository must be an explicit HTTP(S) repository URL"
|
|
97
95
|
end
|
|
96
|
+
unless uri.is_a?(URI::HTTP) && uri.host && !uri.userinfo && !uri.query && !uri.fragment
|
|
97
|
+
raise Invalid, "PR repository must be an explicit HTTP(S) repository URL"
|
|
98
|
+
end
|
|
99
|
+
normalized = Ace::Git::Atoms::ServerUrl.normalize(repository)
|
|
100
|
+
repo = parsed.repo.downcase
|
|
101
|
+
# Retain the exact server/port and optional forge base path. The
|
|
102
|
+
# qualified PR names the final owner/repository pair on that server.
|
|
103
|
+
unless normalized.split("/").drop(1).last(2).join("/") == repo
|
|
104
|
+
raise Invalid, "PR repository identity does not match the declared repository"
|
|
105
|
+
end
|
|
106
|
+
repository = "#{uri.scheme.downcase}://#{normalized}"
|
|
98
107
|
{"repository" => repository, "pr" => "#{repo}##{parsed.number.to_i}"}
|
|
99
108
|
else
|
|
100
109
|
{"repository" => repository, "local_candidate_id" => id!(local, "local_candidate_id")}
|
|
@@ -105,13 +114,16 @@ module Ace
|
|
|
105
114
|
object!(value, "policy")
|
|
106
115
|
required = %w[revision minimum_rounds clean_rounds required_scopes required_checks]
|
|
107
116
|
raise Invalid, "policy requires #{required.join(', ')}" unless (required - value.keys).empty?
|
|
108
|
-
raise Invalid, "unknown policy fields" unless (value.keys - required).empty?
|
|
117
|
+
raise Invalid, "unknown policy fields" unless (value.keys - required - ["maximum_rounds"]).empty?
|
|
109
118
|
string!(value["revision"], "policy revision")
|
|
110
119
|
%w[minimum_rounds clean_rounds].each do |key|
|
|
111
120
|
raise Invalid, "#{key} must be a positive integer" unless value[key].is_a?(Integer) && value[key].positive?
|
|
112
121
|
end
|
|
113
122
|
strings!(value["required_scopes"], "required_scopes").each { |v| id!(v, "scope") }
|
|
114
123
|
strings!(value["required_checks"], "required_checks")
|
|
124
|
+
if value.key?("maximum_rounds") && !(value["maximum_rounds"].is_a?(Integer) && value["maximum_rounds"].positive?)
|
|
125
|
+
raise Invalid, "maximum_rounds must be a positive integer"
|
|
126
|
+
end
|
|
115
127
|
canonical(value)
|
|
116
128
|
end
|
|
117
129
|
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "campaign_contract"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Review
|
|
6
|
+
module Atoms
|
|
7
|
+
# Exploration budgets are independent of the current-head acceptance gate.
|
|
8
|
+
module CampaignPolicy
|
|
9
|
+
Contract = CampaignContract
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
"discovery" => {"minimum_rounds" => 2, "clean_rounds" => 1, "maximum_rounds" => 2},
|
|
12
|
+
"delivery" => {"minimum_rounds" => 3, "clean_rounds" => 2, "maximum_rounds" => 5}
|
|
13
|
+
}.transform_values(&:freeze).freeze
|
|
14
|
+
TERMINAL_FAILURES = %w[authentication authorization model_unavailable unclassified].freeze
|
|
15
|
+
TRANSIENT_FAILURES = %w[timeout transport rate_limit unavailable].freeze
|
|
16
|
+
INFRASTRUCTURE_RETRIES = 2
|
|
17
|
+
|
|
18
|
+
def self.bounds!(profile:, policy:)
|
|
19
|
+
defaults = DEFAULTS[profile]
|
|
20
|
+
raise Contract::Invalid, "unsupported campaign profile #{profile}" unless defaults
|
|
21
|
+
Contract.object!(policy, "campaign policy")
|
|
22
|
+
result = defaults.merge(policy.slice("minimum_rounds", "clean_rounds", "maximum_rounds"))
|
|
23
|
+
unless result.values.all? { |value| value.is_a?(Integer) && value.positive? } &&
|
|
24
|
+
result["minimum_rounds"] <= result["maximum_rounds"] &&
|
|
25
|
+
result["clean_rounds"] <= result["maximum_rounds"] &&
|
|
26
|
+
result["maximum_rounds"] <= defaults["maximum_rounds"]
|
|
27
|
+
raise Contract::Invalid, "campaign policy conflicts with its finite execution budget"
|
|
28
|
+
end
|
|
29
|
+
if profile == "delivery" &&
|
|
30
|
+
(result["minimum_rounds"] < 3 || result["clean_rounds"] < 2)
|
|
31
|
+
raise Contract::Invalid, "delivery policy cannot weaken its search requirements"
|
|
32
|
+
end
|
|
33
|
+
result.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.next_phase(record:, phase_id:, reason:, route:, additional_rounds:)
|
|
37
|
+
Contract.id!(phase_id, "phase ID")
|
|
38
|
+
Contract.string!(reason, "next phase reason")
|
|
39
|
+
Contract.string!(route, "next phase route")
|
|
40
|
+
unless additional_rounds.is_a?(Integer) && additional_rounds.positive?
|
|
41
|
+
raise Contract::Invalid, "next phase requires an explicit finite positive budget"
|
|
42
|
+
end
|
|
43
|
+
existing = record.fetch("phases").find { |phase| phase["id"] == phase_id }
|
|
44
|
+
if existing
|
|
45
|
+
unless existing.values_at("reason", "route", "maximum_rounds") == [reason, route, additional_rounds]
|
|
46
|
+
raise Contract::Invalid, "conflicting replay of authorized next phase"
|
|
47
|
+
end
|
|
48
|
+
return existing
|
|
49
|
+
end
|
|
50
|
+
{"id" => phase_id, "profile" => record.fetch("profile"), "round_start" => record.fetch("rounds").size,
|
|
51
|
+
"maximum_rounds" => additional_rounds, "reason" => reason, "route" => route}.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.decision(profile:, bounds:, phase_rounds:, completed_rounds:, clean_streak:,
|
|
55
|
+
accepted:, recurrence: false, execution_failure: nil)
|
|
56
|
+
raise Contract::Invalid, "unsupported campaign profile #{profile}" unless DEFAULTS.key?(profile)
|
|
57
|
+
unless [phase_rounds, completed_rounds, clean_streak].all? { |n| n.is_a?(Integer) && n >= 0 } &&
|
|
58
|
+
phase_rounds <= completed_rounds && clean_streak <= completed_rounds && phase_rounds <= bounds.fetch("maximum_rounds") &&
|
|
59
|
+
[true, false].include?(accepted) && [true, false].include?(recurrence)
|
|
60
|
+
raise Contract::Invalid, "campaign counters or acceptance differ"
|
|
61
|
+
end
|
|
62
|
+
converged = profile == "delivery" && completed_rounds >= bounds.fetch("minimum_rounds") &&
|
|
63
|
+
clean_streak >= bounds.fetch("clean_rounds")
|
|
64
|
+
remaining = bounds.fetch("maximum_rounds") - phase_rounds
|
|
65
|
+
outcome, action = if profile == "discovery" && remaining.zero?
|
|
66
|
+
["discovery_complete", "export_candidate"]
|
|
67
|
+
elsif execution_failure
|
|
68
|
+
["execution_failed", "resolve_execution_failure"]
|
|
69
|
+
elsif profile == "delivery" && accepted && converged && !recurrence
|
|
70
|
+
["accepted", "finish"]
|
|
71
|
+
elsif remaining.zero?
|
|
72
|
+
["needs_escalation", "authorize_next_phase"]
|
|
73
|
+
elsif recurrence
|
|
74
|
+
["needs_diagnosis", "diagnose_canonical_defect"]
|
|
75
|
+
elsif converged
|
|
76
|
+
["search_converged", "satisfy_acceptance"]
|
|
77
|
+
else
|
|
78
|
+
["in_progress", "review"]
|
|
79
|
+
end
|
|
80
|
+
{"profile" => profile, "outcome" => outcome, "next_action" => action,
|
|
81
|
+
"remaining_rounds" => remaining, "phase_rounds" => phase_rounds,
|
|
82
|
+
"search_converged" => converged, "accepted" => outcome == "accepted",
|
|
83
|
+
"execution_failure" => execution_failure}.freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Severity corrections target the exact observation. A successful fix
|
|
87
|
+
# never retroactively changes the severity of the original round.
|
|
88
|
+
def self.clean_streak(record)
|
|
89
|
+
corrections = record.fetch("assessments").select { |item| item["kind"] == "severity_correction" }
|
|
90
|
+
.to_h { |item| [item.fetch("source_id"), item] }
|
|
91
|
+
rounds = record.fetch("rounds").to_h { |item| [item.fetch("attempt_id"), item] }
|
|
92
|
+
streak = 0
|
|
93
|
+
record.fetch("attempts").each do |attempt|
|
|
94
|
+
blocked = attempt.fetch("assessments").any? do |item|
|
|
95
|
+
correction = corrections[item.fetch("source_id")]
|
|
96
|
+
priority = correction ? correction.fetch("priority") : item.fetch("priority")
|
|
97
|
+
disposition = correction ? correction.fetch("disposition") : item.fetch("disposition")
|
|
98
|
+
item["observed_in_round"] && %w[high critical].include?(priority) && disposition != "invalid"
|
|
99
|
+
end
|
|
100
|
+
streak = 0 if blocked
|
|
101
|
+
if rounds.key?(attempt.fetch("attempt_id"))
|
|
102
|
+
round_observations = record.fetch("attempts").select { |item| attempt["round_id"] ? item["round_id"] == attempt["round_id"] : item["attempt_id"] == attempt["attempt_id"] }
|
|
103
|
+
.flat_map { |item| item.fetch("assessments") }
|
|
104
|
+
round_blocked = round_observations.any? do |item|
|
|
105
|
+
correction = corrections[item.fetch("source_id")]
|
|
106
|
+
priority = correction ? correction.fetch("priority") : item.fetch("priority")
|
|
107
|
+
disposition = correction ? correction.fetch("disposition") : item.fetch("disposition")
|
|
108
|
+
item["observed_in_round"] && %w[high critical].include?(priority) && disposition != "invalid"
|
|
109
|
+
end
|
|
110
|
+
streak = round_blocked ? 0 : streak + 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
streak
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.canonical_findings(record)
|
|
117
|
+
findings = {}
|
|
118
|
+
(record.fetch("inherited_findings") + record.fetch("assessments")).each do |item|
|
|
119
|
+
next if item["kind"] == "repair_attempt"
|
|
120
|
+
id = item.fetch("id")
|
|
121
|
+
prior = findings[id]
|
|
122
|
+
sources = prior ? prior.fetch("sources") : item.fetch("sources", []).dup
|
|
123
|
+
source = item.slice("source_id", "artifact", "source_artifact", "priority", "disposition", "reason")
|
|
124
|
+
sources = sources + [source] unless sources.include?(source)
|
|
125
|
+
findings[id] = item.merge("sources" => sources)
|
|
126
|
+
end
|
|
127
|
+
findings.values
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.recurring_blockers(record)
|
|
131
|
+
fixes = {}
|
|
132
|
+
recurring = {}
|
|
133
|
+
observed = []
|
|
134
|
+
record.fetch("assessments").each do |item|
|
|
135
|
+
id = item.fetch("id")
|
|
136
|
+
observation = item.slice("source_id", "artifact")
|
|
137
|
+
fresh_observation = !observed.include?(observation)
|
|
138
|
+
observed << observation if item["observed_in_round"] && fresh_observation
|
|
139
|
+
if item["kind"] == "repair_attempt"
|
|
140
|
+
fixes[id] = item
|
|
141
|
+
elsif %w[resolved invalid].include?(item["disposition"])
|
|
142
|
+
fixes.delete(id)
|
|
143
|
+
recurring.delete(id)
|
|
144
|
+
elsif item["observed_in_round"] && fresh_observation &&
|
|
145
|
+
%w[high critical].include?(item["priority"]) && fixes.key?(id)
|
|
146
|
+
recurring[id] = {"finding_id" => id, "repair_attempt" => fixes.fetch(id), "recurrence" => item}
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
recurring.values
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Entries are the durable before-call reservations, not report imports.
|
|
153
|
+
# An unanswered original call requires resolution, never a fresh retry.
|
|
154
|
+
def self.retry_decision(attempts:, round_id:, scope:, provider:)
|
|
155
|
+
[round_id, scope, provider].each { |value| Contract.string!(value, "execution selection") }
|
|
156
|
+
raise Contract::Invalid, "execution attempts must be an array" unless attempts.is_a?(Array)
|
|
157
|
+
attempts.each { |entry| Contract.object!(entry, "execution attempt") }
|
|
158
|
+
selected = attempts.select do |entry|
|
|
159
|
+
entry.values_at("round_id", "scope", "provider") == [round_id, scope, provider]
|
|
160
|
+
end
|
|
161
|
+
selected.each do |entry|
|
|
162
|
+
unless %w[running uncertain succeeded failed].include?(entry["status"]) &&
|
|
163
|
+
(entry["status"] != "failed" || (TERMINAL_FAILURES + TRANSIENT_FAILURES).include?(entry["failure"]))
|
|
164
|
+
raise Contract::Invalid, "execution attempt disposition differs"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
state = if selected.any? { |entry| %w[running uncertain].include?(entry["status"]) }
|
|
168
|
+
"resolve_original_attempt"
|
|
169
|
+
elsif selected.any? { |entry| entry["status"] == "succeeded" }
|
|
170
|
+
"already_completed"
|
|
171
|
+
elsif selected.any? { |entry| TERMINAL_FAILURES.include?(entry["failure"]) }
|
|
172
|
+
"terminal_failure"
|
|
173
|
+
elsif selected.size >= INFRASTRUCTURE_RETRIES + 1
|
|
174
|
+
"retries_exhausted"
|
|
175
|
+
else
|
|
176
|
+
"execute"
|
|
177
|
+
end
|
|
178
|
+
{"next_action" => state, "execution_attempts" => selected.size,
|
|
179
|
+
"remaining_attempts" => [INFRASTRUCTURE_RETRIES + 1 - selected.size, 0].max}.freeze
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require_relative "campaign_policy"
|
|
2
3
|
|
|
3
4
|
module Ace
|
|
4
5
|
module Review
|
|
@@ -7,32 +8,28 @@ module Ace
|
|
|
7
8
|
module CampaignProjection
|
|
8
9
|
def self.build(record)
|
|
9
10
|
rounds = record.fetch("rounds")
|
|
10
|
-
findings =
|
|
11
|
-
|
|
12
|
-
open = findings.values.select { |finding| %w[open reopened].include?(finding["disposition"]) }
|
|
11
|
+
findings = CampaignPolicy.canonical_findings(record)
|
|
12
|
+
open = findings.select { |finding| %w[open reopened].include?(finding["disposition"]) }
|
|
13
13
|
policy = record.fetch("policy")
|
|
14
14
|
attempts = record.fetch("attempts")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
%w[high critical].include?(finding["priority"]) && finding["disposition"] != "invalid" }
|
|
22
|
-
streak = 0
|
|
23
|
-
end
|
|
24
|
-
round = completed[attempt.fetch("attempt_id")]
|
|
25
|
-
streak = round["clean"] ? streak + 1 : 0 if round
|
|
26
|
-
end
|
|
15
|
+
streak = CampaignPolicy.clean_streak(record)
|
|
16
|
+
phase = record.fetch("phases").last
|
|
17
|
+
bounds = record.fetch("bounds").merge("maximum_rounds" => phase.fetch("maximum_rounds"))
|
|
18
|
+
decision = CampaignPolicy.decision(profile: record.fetch("profile"), bounds: bounds,
|
|
19
|
+
phase_rounds: rounds.size - phase.fetch("round_start"), completed_rounds: rounds.size,
|
|
20
|
+
clean_streak: streak, accepted: false, recurrence: !CampaignPolicy.recurring_blockers(record).empty?)
|
|
27
21
|
sessions = attempts.flat_map { |attempt| attempt.fetch("sessions") }.uniq { |session| session["path"] }
|
|
28
22
|
{"campaign_id" => record.fetch("id"), "subject" => record.fetch("subject"),
|
|
29
23
|
"contract_identity" => record.fetch("contract_identity"), "effective_policy" => policy,
|
|
30
24
|
"predecessor" => record["predecessor"], "successor_reason" => record["successor_reason"],
|
|
31
25
|
"required_scopes" => policy.fetch("required_scopes"), "completed_rounds" => rounds.size,
|
|
32
|
-
"clean_streak" => streak, "open_findings" => open, "findings" => findings
|
|
33
|
-
"search_converged" =>
|
|
34
|
-
|
|
35
|
-
"
|
|
26
|
+
"clean_streak" => streak, "open_findings" => open, "findings" => findings,
|
|
27
|
+
"search_converged" => decision.fetch("search_converged"),
|
|
28
|
+
"profile" => record.fetch("profile"), "phase" => phase, "phases" => record.fetch("phases"),
|
|
29
|
+
"bounds" => record.fetch("bounds"), "decision" => decision,
|
|
30
|
+
"execution_attempts" => record.fetch("execution_attempts"),
|
|
31
|
+
"recurring_blockers" => CampaignPolicy.recurring_blockers(record),
|
|
32
|
+
"counters" => {"recording_attempts" => attempts.size, "execution_attempts" => record.fetch("execution_attempts").size,
|
|
36
33
|
"provider_calls" => sessions.sum { |s| s.fetch("provider_calls") },
|
|
37
34
|
"report_files" => sessions.sum { |s| s.fetch("report_files") }, "completed_rounds" => rounds.size},
|
|
38
35
|
"rounds" => rounds, "attempts" => attempts, "head_transitions" => record.fetch("head_transitions")}
|
|
@@ -3,6 +3,8 @@ require_relative "commands/campaign/start"
|
|
|
3
3
|
require_relative "commands/campaign/status"
|
|
4
4
|
require_relative "commands/campaign/record_round"
|
|
5
5
|
require_relative "commands/campaign/finish"
|
|
6
|
+
require_relative "commands/campaign/resume"
|
|
7
|
+
require_relative "commands/campaign/assess"
|
|
6
8
|
|
|
7
9
|
module Ace
|
|
8
10
|
module Review
|
|
@@ -11,7 +13,7 @@ module Ace
|
|
|
11
13
|
extend Ace::Support::Cli::RegistryDsl
|
|
12
14
|
PROGRAM_NAME = "ace-review campaign"
|
|
13
15
|
REGISTERED_COMMANDS = [["start", "Start/reuse a frozen campaign"], ["status", "Inspect history and evidence"],
|
|
14
|
-
["record-round", "Pin or record scoped evidence"], ["finish", "Validate current acceptance"]].freeze
|
|
16
|
+
["record-round", "Pin or record scoped evidence"], ["finish", "Validate current acceptance"], ["resume", "Authorize a finite next phase"], ["assess", "Record verified finding assessment"]].freeze
|
|
15
17
|
HELP_EXAMPLES = ["ace-review campaign start --subject subject.json --contract requirements.md --profile delivery",
|
|
16
18
|
"ace-review campaign status ID --format json", "ace-review campaign record-round ID --input round.json",
|
|
17
19
|
"ace-review campaign finish ID --format json"].freeze
|
|
@@ -19,9 +21,11 @@ module Ace
|
|
|
19
21
|
register "status", Commands::Campaign::Status
|
|
20
22
|
register "record-round", Commands::Campaign::RecordRound
|
|
21
23
|
register "finish", Commands::Campaign::Finish
|
|
24
|
+
register "resume", Commands::Campaign::Resume
|
|
25
|
+
register "assess", Commands::Campaign::Assess
|
|
22
26
|
help = Ace::Support::Cli::HelpCommand.build(program_name: PROGRAM_NAME, version: Ace::Review::VERSION,
|
|
23
27
|
commands: [["start", "Start/reuse a frozen campaign"], ["status", "Inspect history and evidence"],
|
|
24
|
-
["record-round", "Pin or record scoped evidence"], ["finish", "Validate current acceptance"]],
|
|
28
|
+
["record-round", "Pin or record scoped evidence"], ["finish", "Validate current acceptance"], ["resume", "Authorize a finite next phase"], ["assess", "Record verified finding assessment"]],
|
|
25
29
|
examples: ["ace-review campaign start --subject subject.json --contract requirements.md --profile delivery",
|
|
26
30
|
"ace-review campaign status ID --format json", "ace-review campaign record-round ID --input round.json",
|
|
27
31
|
"ace-review campaign finish ID --format json"])
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "base"
|
|
3
|
+
module Ace
|
|
4
|
+
module Review
|
|
5
|
+
module CLI
|
|
6
|
+
module Commands
|
|
7
|
+
module Campaign
|
|
8
|
+
class Assess < Base
|
|
9
|
+
desc "Record verified severity correction or evidenced repair attempt"
|
|
10
|
+
argument :id, required: true, desc: "Durable campaign ID"
|
|
11
|
+
option :input, required: true, desc: "Assessment JSON with source identity, kind, reason and evidence"
|
|
12
|
+
|
|
13
|
+
def call(id:, **options)
|
|
14
|
+
run(options) { |manager| manager.assess_finding(id, read_json(options[:input]), dry_run: options[:dry_run]) }
|
|
15
|
+
0
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|