rvw 0.1.0__tar.gz

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.
rvw-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soju06
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rvw-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: rvw
3
+ Version: 0.1.0
4
+ Summary: Layered, replicated, self-adjudicating code review orchestrator
5
+ Keywords: cli,code-review,llm,orchestrator,static-analysis
6
+ Author: Soju06
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Dist: typer>=0.27.0
19
+ Requires-Dist: rich>=15.0.0
20
+ Requires-Dist: pydantic>=2.13.0
21
+ Requires-Dist: pyyaml>=6.0.3
22
+ Requires-Dist: pytest>=9.1.0 ; extra == 'dev'
23
+ Requires-Dist: pytest-asyncio>=1.4.0 ; extra == 'dev'
24
+ Requires-Dist: pytest-cov>=7.1.0 ; extra == 'dev'
25
+ Requires-Dist: ruff>=0.16.0 ; extra == 'dev'
26
+ Requires-Dist: ty>=0.0.63 ; extra == 'dev'
27
+ Requires-Dist: pre-commit>=4.6.0 ; extra == 'dev'
28
+ Requires-Dist: types-pyyaml>=6.0.12 ; extra == 'dev'
29
+ Requires-Python: >=3.12
30
+ Project-URL: Repository, https://github.com/Soju06/rvw
31
+ Project-URL: Issues, https://github.com/Soju06/rvw/issues
32
+ Project-URL: Changelog, https://github.com/Soju06/rvw/releases
33
+ Provides-Extra: dev
34
+ Description-Content-Type: text/markdown
35
+
36
+ # rvw
37
+
38
+ [![CI](https://github.com/Soju06/rvw/actions/workflows/ci.yml/badge.svg)](https://github.com/Soju06/rvw/actions/workflows/ci.yml)
39
+ [![PyPI](https://img.shields.io/pypi/v/rvw)](https://pypi.org/project/rvw/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/rvw)](https://pypi.org/project/rvw/)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
42
+
43
+ **Layered, replicated, self-adjudicating code review orchestrator.**
44
+
45
+ `rvw` turns LLM code review from a single noisy pass into a deterministic
46
+ pipeline: activate rule lanes in layers, fire every lane with N replicas in one
47
+ concurrent wave, merge findings by content-derived keys, adjudicate each
48
+ candidate against the actual source, and publish one synthesized report.
49
+
50
+ ```
51
+ DISCOVER ──▶ MERGE ──▶ ADJUDICATE ──▶ REPORT ──▶ publish
52
+ lanes × collapse replicas vote Korean md GitHub review
53
+ replicas + folds on real source + coverage (inline anchors)
54
+ ```
55
+
56
+ ## Why
57
+
58
+ Single-pass LLM review has three structural problems, each addressed by a
59
+ measured design decision (see [DECISIONS.md](DECISIONS.md)):
60
+
61
+ | Problem | Mechanism | Measured |
62
+ |---|---|---|
63
+ | One pass misses findings | 3 replicas per lane, one wave | recall 88% → 99% (ADR-006) |
64
+ | Scoped rules go blind outside their scope | mandatory `unscoped-sweep` lane | 3/3 deep defects only the sweep caught (ADR-005) |
65
+ | LLMs fabricate findings | separate adjudication lane, votes grounded in real source | 3/3 fabricated rejected, 0/6 genuine lost (ADR-007) |
66
+
67
+ Findings are forced through closed rule enums via strict `--output-schema` —
68
+ measured: the schema beats the prompt, and closed enums cost zero recall vs
69
+ free-form ids (ADR-004).
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install rvw # or: uv tool install rvw
75
+ ```
76
+
77
+ Requires Python 3.12+ and a working [Codex CLI](https://github.com/openai/codex)
78
+ (`codex exec`) as the review runtime.
79
+
80
+ ## Quickstart
81
+
82
+ ```bash
83
+ # One command: resolve PR → discover → merge → adjudicate → report
84
+ rvw review --target 1119 --repo-dir /path/to/pr-head-checkout
85
+
86
+ # Plan only (no execution): which lanes activate, how many runs
87
+ rvw plan --target 1119 --json
88
+
89
+ # Deterministic gate for CI: exit 0 PASS / 1 BLOCK
90
+ rvw auto --target 1119 --repo-dir /path/to/checkout
91
+
92
+ # Publish the report as a GitHub review (dry-run by default)
93
+ rvw publish --run <run-id> --execute
94
+ ```
95
+
96
+ ## Concepts
97
+
98
+ - **Rule** — one atomic check (`slop/sot-violation`, `bug/severe-defect`, …)
99
+ - **Lane** — a named rule bundle + prompt executed as one review pass; rules
100
+ form the closed output enum
101
+ - **Layer** — activation tier owning lanes: `base` (always) → `project`
102
+ (repo predicate) → `scope` (path predicate) → `dynamic` (per-PR brief)
103
+ - **Runtime** — the execution engine (`codex exec --sandbox read-only`)
104
+ - **Run** — lane × runtime × replica
105
+
106
+ The registry lives outside the package (default `~/.hermes/review/`):
107
+ `layers.yaml` + `lanes/**.md` (YAML frontmatter + prompt body) + `policies/`.
108
+ Everything is referenced by name so documents, lanes, and runtimes can be
109
+ swapped without touching code.
110
+
111
+ ## Pipeline guarantees
112
+
113
+ - **Validity contract** — a run counts only if: exit 0, artifact exists,
114
+ strict-schema JSON validates, completion marker present. INVALID replicas
115
+ are never promoted; an all-INVALID lane is re-dispatched exactly once.
116
+ - **Deterministic merge** — findings collapse by `(file, hunk, rule)`;
117
+ cross-lane corroboration and replica agreement are computed, not guessed.
118
+ Display folds (same-pattern-across-files, same-region) never merge
119
+ verdicts and never chain transitively.
120
+ - **Source-grounded verdicts** — adjudication replicas run inside the target
121
+ checkout; REJECTED requires a verbatim disproving source quote, otherwise
122
+ it is coerced to UNCERTAIN. Unresolved candidates are reported as
123
+ unverified, never silently dropped.
124
+ - **Approve is not expressible** — the publish layer hardcodes COMMENT.
125
+ `rvw auto` decides PASS/BLOCK from a YAML threshold policy; nothing in the
126
+ pipeline can emit an approving review.
127
+
128
+ ## Lane health
129
+
130
+ ```bash
131
+ rvw lanes list # registry overview
132
+ rvw sample --lane slop-hygiene --fixture tests/fixtures/deep.ts
133
+ # enum-vs-free A/B gate for new lanes
134
+ rvw doctor # INVALID rate, /other rate, rejection rate
135
+ ```
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ git clone https://github.com/Soju06/rvw && cd rvw
141
+ uv sync --all-extras
142
+ uv run pytest -q -m "not live" # unit suite
143
+ uv run pytest -q -m live # exercises real codex (needs credentials)
144
+ ```
145
+
146
+ Gates: `ruff check` · `ruff format` · `ty check` · `pytest`. Architecture
147
+ decisions are append-only in [DECISIONS.md](DECISIONS.md).
148
+
149
+ ## License
150
+
151
+ [MIT](LICENSE)
rvw-0.1.0/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # rvw
2
+
3
+ [![CI](https://github.com/Soju06/rvw/actions/workflows/ci.yml/badge.svg)](https://github.com/Soju06/rvw/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/rvw)](https://pypi.org/project/rvw/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/rvw)](https://pypi.org/project/rvw/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ **Layered, replicated, self-adjudicating code review orchestrator.**
9
+
10
+ `rvw` turns LLM code review from a single noisy pass into a deterministic
11
+ pipeline: activate rule lanes in layers, fire every lane with N replicas in one
12
+ concurrent wave, merge findings by content-derived keys, adjudicate each
13
+ candidate against the actual source, and publish one synthesized report.
14
+
15
+ ```
16
+ DISCOVER ──▶ MERGE ──▶ ADJUDICATE ──▶ REPORT ──▶ publish
17
+ lanes × collapse replicas vote Korean md GitHub review
18
+ replicas + folds on real source + coverage (inline anchors)
19
+ ```
20
+
21
+ ## Why
22
+
23
+ Single-pass LLM review has three structural problems, each addressed by a
24
+ measured design decision (see [DECISIONS.md](DECISIONS.md)):
25
+
26
+ | Problem | Mechanism | Measured |
27
+ |---|---|---|
28
+ | One pass misses findings | 3 replicas per lane, one wave | recall 88% → 99% (ADR-006) |
29
+ | Scoped rules go blind outside their scope | mandatory `unscoped-sweep` lane | 3/3 deep defects only the sweep caught (ADR-005) |
30
+ | LLMs fabricate findings | separate adjudication lane, votes grounded in real source | 3/3 fabricated rejected, 0/6 genuine lost (ADR-007) |
31
+
32
+ Findings are forced through closed rule enums via strict `--output-schema` —
33
+ measured: the schema beats the prompt, and closed enums cost zero recall vs
34
+ free-form ids (ADR-004).
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install rvw # or: uv tool install rvw
40
+ ```
41
+
42
+ Requires Python 3.12+ and a working [Codex CLI](https://github.com/openai/codex)
43
+ (`codex exec`) as the review runtime.
44
+
45
+ ## Quickstart
46
+
47
+ ```bash
48
+ # One command: resolve PR → discover → merge → adjudicate → report
49
+ rvw review --target 1119 --repo-dir /path/to/pr-head-checkout
50
+
51
+ # Plan only (no execution): which lanes activate, how many runs
52
+ rvw plan --target 1119 --json
53
+
54
+ # Deterministic gate for CI: exit 0 PASS / 1 BLOCK
55
+ rvw auto --target 1119 --repo-dir /path/to/checkout
56
+
57
+ # Publish the report as a GitHub review (dry-run by default)
58
+ rvw publish --run <run-id> --execute
59
+ ```
60
+
61
+ ## Concepts
62
+
63
+ - **Rule** — one atomic check (`slop/sot-violation`, `bug/severe-defect`, …)
64
+ - **Lane** — a named rule bundle + prompt executed as one review pass; rules
65
+ form the closed output enum
66
+ - **Layer** — activation tier owning lanes: `base` (always) → `project`
67
+ (repo predicate) → `scope` (path predicate) → `dynamic` (per-PR brief)
68
+ - **Runtime** — the execution engine (`codex exec --sandbox read-only`)
69
+ - **Run** — lane × runtime × replica
70
+
71
+ The registry lives outside the package (default `~/.hermes/review/`):
72
+ `layers.yaml` + `lanes/**.md` (YAML frontmatter + prompt body) + `policies/`.
73
+ Everything is referenced by name so documents, lanes, and runtimes can be
74
+ swapped without touching code.
75
+
76
+ ## Pipeline guarantees
77
+
78
+ - **Validity contract** — a run counts only if: exit 0, artifact exists,
79
+ strict-schema JSON validates, completion marker present. INVALID replicas
80
+ are never promoted; an all-INVALID lane is re-dispatched exactly once.
81
+ - **Deterministic merge** — findings collapse by `(file, hunk, rule)`;
82
+ cross-lane corroboration and replica agreement are computed, not guessed.
83
+ Display folds (same-pattern-across-files, same-region) never merge
84
+ verdicts and never chain transitively.
85
+ - **Source-grounded verdicts** — adjudication replicas run inside the target
86
+ checkout; REJECTED requires a verbatim disproving source quote, otherwise
87
+ it is coerced to UNCERTAIN. Unresolved candidates are reported as
88
+ unverified, never silently dropped.
89
+ - **Approve is not expressible** — the publish layer hardcodes COMMENT.
90
+ `rvw auto` decides PASS/BLOCK from a YAML threshold policy; nothing in the
91
+ pipeline can emit an approving review.
92
+
93
+ ## Lane health
94
+
95
+ ```bash
96
+ rvw lanes list # registry overview
97
+ rvw sample --lane slop-hygiene --fixture tests/fixtures/deep.ts
98
+ # enum-vs-free A/B gate for new lanes
99
+ rvw doctor # INVALID rate, /other rate, rejection rate
100
+ ```
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ git clone https://github.com/Soju06/rvw && cd rvw
106
+ uv sync --all-extras
107
+ uv run pytest -q -m "not live" # unit suite
108
+ uv run pytest -q -m live # exercises real codex (needs credentials)
109
+ ```
110
+
111
+ Gates: `ruff check` · `ruff format` · `ty check` · `pytest`. Architecture
112
+ decisions are append-only in [DECISIONS.md](DECISIONS.md).
113
+
114
+ ## License
115
+
116
+ [MIT](LICENSE)
@@ -0,0 +1,90 @@
1
+ [project]
2
+ name = "rvw"
3
+ version = "0.1.0"
4
+ description = "Layered, replicated, self-adjudicating code review orchestrator"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Soju06" }]
9
+ requires-python = ">=3.12"
10
+ keywords = ["cli", "code-review", "llm", "orchestrator", "static-analysis"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "Operating System :: POSIX :: Linux",
16
+ "Operating System :: MacOS",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ dependencies = [
23
+ "typer>=0.27.0",
24
+ "rich>=15.0.0",
25
+ "pydantic>=2.13.0",
26
+ "pyyaml>=6.0.3",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=9.1.0",
32
+ "pytest-asyncio>=1.4.0",
33
+ "pytest-cov>=7.1.0",
34
+ "ruff>=0.16.0",
35
+ "ty>=0.0.63",
36
+ "pre-commit>=4.6.0",
37
+ "types-pyyaml>=6.0.12",
38
+ ]
39
+
40
+ [project.scripts]
41
+ # IMPORTANT: target the Typer app object, not a main() function.
42
+ rvw = "rvw.cli:app"
43
+
44
+ [project.urls]
45
+ Repository = "https://github.com/Soju06/rvw"
46
+ Issues = "https://github.com/Soju06/rvw/issues"
47
+ Changelog = "https://github.com/Soju06/rvw/releases"
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.10.9,<0.13.0"]
51
+ build-backend = "uv_build"
52
+
53
+ [tool.ruff]
54
+ line-length = 100
55
+ target-version = "py312"
56
+ extend-exclude = [".hermes"]
57
+
58
+ [tool.ruff.lint]
59
+ select = [
60
+ "E", # pycodestyle errors
61
+ "W", # pycodestyle warnings
62
+ "F", # pyflakes
63
+ "I", # isort
64
+ "B", # flake8-bugbear
65
+ "UP", # pyupgrade
66
+ "SIM", # flake8-simplify
67
+ "RUF", # ruff-specific
68
+ ]
69
+ ignore = [
70
+ "E501", # line too long (formatter handles)
71
+ ]
72
+
73
+ [tool.ruff.format]
74
+ quote-style = "double"
75
+ indent-style = "space"
76
+
77
+ [tool.ty.environment]
78
+ python-version = "3.12"
79
+
80
+ [tool.ty.src]
81
+ include = ["src", "tests"]
82
+
83
+ [tool.pytest.ini_options]
84
+ minversion = "8.0"
85
+ addopts = "-ra --strict-markers"
86
+ testpaths = ["tests"]
87
+ asyncio_mode = "auto"
88
+ markers = [
89
+ "live: requires a real codex CLI invocation (deselect with -m 'not live')",
90
+ ]
@@ -0,0 +1,10 @@
1
+ """rvw package layout.
2
+
3
+ The command surface lives in ``cli`` and wire models in ``schema``. Registry,
4
+ lane, target, hunks, dispatch, and runtimes modules implement review planning
5
+ and execution. Architectural decisions are recorded in ``DECISIONS.md``.
6
+ """
7
+
8
+ from rvw._version import __version__
9
+
10
+ __all__: list[str] = ["__version__"]
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the rvw version string."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,282 @@
1
+ """Evidence-bearing, majority-voted adjudication of merged findings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections import Counter
7
+ from collections.abc import Sequence
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, cast
11
+
12
+ from rvw.merge import CollapseGroup, MergeResult
13
+ from rvw.runtimes import RunResult, RunStatus, Runtime
14
+ from rvw.schema import RuntimeAdjudication, RuntimeAdjudicationItem, Verdict
15
+ from rvw.target import ResolvedTarget
16
+
17
+
18
+ def adjudication_schema(group_keys: Sequence[str]) -> dict[str, Any]:
19
+ """Build the closed, OpenAI-strict schema for one candidate batch."""
20
+
21
+ schema = RuntimeAdjudication.model_json_schema()
22
+ schema.pop("$defs", None)
23
+ item_properties = dict(RuntimeAdjudicationItem.model_json_schema()["properties"])
24
+ item_properties["group_key"] = {"type": "string", "enum": list(group_keys)}
25
+ item_properties["verdict"] = {
26
+ "type": "string",
27
+ "enum": [verdict.value for verdict in Verdict],
28
+ }
29
+ schema["additionalProperties"] = False
30
+ schema["required"] = list(schema["properties"])
31
+ schema["properties"]["items"]["items"] = {
32
+ "type": "object",
33
+ "properties": item_properties,
34
+ "required": list(item_properties),
35
+ "additionalProperties": False,
36
+ }
37
+ return schema
38
+
39
+
40
+ def build_adjudication_prompt(groups: Sequence[CollapseGroup], *, diff: str, expanded: bool) -> str:
41
+ """Render an adjudication-only prompt with every replica body preserved."""
42
+
43
+ parts = [
44
+ "# Role",
45
+ (
46
+ "You are an adjudicator, not a reviewer. For each candidate finding, decide from "
47
+ "the ACTUAL SOURCE in this working directory whether it is real. Do not report new "
48
+ "findings."
49
+ ),
50
+ "# Verdict contract",
51
+ (
52
+ "CONFIRMED = the defect is real at HEAD; evidence must quote the offending source "
53
+ "line(s) verbatim."
54
+ ),
55
+ (
56
+ "REJECTED = the claim is factually wrong; evidence MUST quote the disproving source "
57
+ "line(s) verbatim."
58
+ ),
59
+ "UNCERTAIN = you cannot decide from the available context.",
60
+ (
61
+ "Never guess: an unverifiable claim is UNCERTAIN, not REJECTED. Return exactly one "
62
+ "verdict for each supplied candidate and no other findings."
63
+ ),
64
+ ]
65
+ if expanded:
66
+ parts.extend(
67
+ [
68
+ "# Expanded context",
69
+ (
70
+ "EXPANDED CONTEXT PASS: you may and should explore beyond the diff — read "
71
+ "the full enclosing function/class, find the symbol's definition and its "
72
+ "callers (grep), and check tests covering the path, before deciding. Prior "
73
+ "pass returned UNCERTAIN for these."
74
+ ),
75
+ ]
76
+ )
77
+
78
+ parts.append("# Candidates")
79
+ for group in groups:
80
+ parts.extend(
81
+ [
82
+ f"## Candidate {group.key}",
83
+ f"group_key: {group.key}",
84
+ f"rule_id: {group.rule_id}",
85
+ f"location: {group.file}:{group.line if group.line is not None else 'unknown'}",
86
+ "Replica reports (verbatim):",
87
+ ]
88
+ )
89
+ for index, body in enumerate(group.bodies, start=1):
90
+ parts.extend([f"### Body {index}", body])
91
+
92
+ parts.extend(["# Unified diff", "```diff", diff, "```"])
93
+ return "\n\n".join(parts)
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class AdjudicationOutcome:
98
+ verdicts: dict[str, Verdict]
99
+ reasons: dict[str, str]
100
+ evidence: dict[str, str]
101
+ replica_votes: dict[str, list[Verdict]]
102
+ unresolved: list[str]
103
+ coerced_rejections: int
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class _Vote:
108
+ verdict: Verdict
109
+ reason: str
110
+ evidence: str
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class _BatchVote:
115
+ verdicts: dict[str, Verdict]
116
+ reasons: dict[str, str]
117
+ evidence: dict[str, str]
118
+ replica_votes: dict[str, list[Verdict]]
119
+ coerced_rejections: int
120
+
121
+
122
+ def _vote_batch(groups: Sequence[CollapseGroup], results: Sequence[RunResult[Any]]) -> _BatchVote:
123
+ valid_outputs = [
124
+ cast(RuntimeAdjudication, result.output)
125
+ for result in results
126
+ if result.status is RunStatus.VALID and result.output is not None
127
+ ]
128
+ votes_by_key: dict[str, list[_Vote]] = {group.key: [] for group in groups}
129
+ coerced_rejections = 0
130
+ for output in valid_outputs:
131
+ by_key: dict[str, RuntimeAdjudicationItem] = {}
132
+ for adjudication_item in output.items:
133
+ by_key.setdefault(adjudication_item.group_key, adjudication_item)
134
+ for group in groups:
135
+ adjudication_item = by_key.get(group.key)
136
+ if adjudication_item is None:
137
+ votes_by_key[group.key].append(_Vote(Verdict.UNCERTAIN, "", ""))
138
+ continue
139
+ verdict = adjudication_item.verdict
140
+ if verdict is Verdict.REJECTED and not adjudication_item.evidence.strip():
141
+ verdict = Verdict.UNCERTAIN
142
+ coerced_rejections += 1
143
+ votes_by_key[group.key].append(
144
+ _Vote(verdict, adjudication_item.reason, adjudication_item.evidence)
145
+ )
146
+
147
+ verdicts: dict[str, Verdict] = {}
148
+ reasons: dict[str, str] = {}
149
+ evidence: dict[str, str] = {}
150
+ replica_votes: dict[str, list[Verdict]] = {}
151
+ for group in groups:
152
+ group_votes = votes_by_key[group.key]
153
+ replica_votes[group.key] = [vote.verdict for vote in group_votes]
154
+ counts = Counter(vote.verdict for vote in group_votes)
155
+ verdict = Verdict.UNCERTAIN
156
+ for candidate in Verdict:
157
+ if counts[candidate] > len(group_votes) / 2:
158
+ verdict = candidate
159
+ break
160
+ verdicts[group.key] = verdict
161
+ supporting_vote = next((vote for vote in group_votes if vote.verdict is verdict), None)
162
+ reasons[group.key] = supporting_vote.reason if supporting_vote is not None else ""
163
+ evidence[group.key] = supporting_vote.evidence if supporting_vote is not None else ""
164
+
165
+ return _BatchVote(
166
+ verdicts=verdicts,
167
+ reasons=reasons,
168
+ evidence=evidence,
169
+ replica_votes=replica_votes,
170
+ coerced_rejections=coerced_rejections,
171
+ )
172
+
173
+
174
+ async def adjudicate(
175
+ merged: MergeResult,
176
+ *,
177
+ target: ResolvedTarget,
178
+ runtime: Runtime,
179
+ repo_dir: Path,
180
+ out_root: Path,
181
+ replicas: int = 3,
182
+ deadline_seconds: int = 600,
183
+ concurrency: int = 16,
184
+ ) -> AdjudicationOutcome:
185
+ """Adjudicate all collapse groups, widening context once for uncertainty."""
186
+
187
+ if replicas < 1:
188
+ raise ValueError("replicas must be at least 1")
189
+ if deadline_seconds < 1:
190
+ raise ValueError("deadline_seconds must be at least 1")
191
+ if concurrency < 1:
192
+ raise ValueError("concurrency must be at least 1")
193
+ if not merged.groups:
194
+ return AdjudicationOutcome({}, {}, {}, {}, [], 0)
195
+
196
+ semaphore = asyncio.Semaphore(concurrency)
197
+
198
+ async def execute_wave(
199
+ groups: Sequence[CollapseGroup], *, expanded: bool, label: str, deadline: int
200
+ ) -> list[RunResult[Any]]:
201
+ prompt = build_adjudication_prompt(groups, diff=target.diff, expanded=expanded)
202
+ schema = adjudication_schema([group.key for group in groups])
203
+
204
+ async def execute_one(replica: int) -> RunResult[Any]:
205
+ async with semaphore:
206
+ run_dir = out_root / label / f"r{replica}"
207
+ return await runtime.execute_raw(
208
+ schema=schema,
209
+ prompt=prompt,
210
+ run_dir=run_dir,
211
+ deadline_seconds=deadline,
212
+ workdir=repo_dir,
213
+ validate=RuntimeAdjudication.model_validate,
214
+ )
215
+
216
+ return list(
217
+ await asyncio.gather(
218
+ *(asyncio.create_task(execute_one(replica)) for replica in range(1, replicas + 1))
219
+ )
220
+ )
221
+
222
+ async def execute_pass(
223
+ groups: Sequence[CollapseGroup], *, expanded: bool, label: str, deadline: int
224
+ ) -> list[RunResult[Any]]:
225
+ results = await execute_wave(groups, expanded=expanded, label=label, deadline=deadline)
226
+ if all(result.status is RunStatus.INVALID for result in results):
227
+ return await execute_wave(
228
+ groups,
229
+ expanded=expanded,
230
+ label=f"{label}-retry",
231
+ deadline=deadline,
232
+ )
233
+ return results
234
+
235
+ initial_results = await execute_pass(
236
+ merged.groups,
237
+ expanded=False,
238
+ label="initial",
239
+ deadline=deadline_seconds,
240
+ )
241
+ initial = _vote_batch(merged.groups, initial_results)
242
+ verdicts = dict(initial.verdicts)
243
+ reasons = dict(initial.reasons)
244
+ evidence = dict(initial.evidence)
245
+ replica_votes = {key: list(votes) for key, votes in initial.replica_votes.items()}
246
+ coerced_rejections = initial.coerced_rejections
247
+
248
+ uncertain_groups = [
249
+ group for group in merged.groups if verdicts[group.key] is Verdict.UNCERTAIN
250
+ ]
251
+ if uncertain_groups:
252
+ expanded_results = await execute_pass(
253
+ uncertain_groups,
254
+ expanded=True,
255
+ label="expanded",
256
+ deadline=deadline_seconds * 2,
257
+ )
258
+ expanded_vote = _vote_batch(uncertain_groups, expanded_results)
259
+ coerced_rejections += expanded_vote.coerced_rejections
260
+ for group in uncertain_groups:
261
+ verdicts[group.key] = expanded_vote.verdicts[group.key]
262
+ reasons[group.key] = expanded_vote.reasons[group.key]
263
+ evidence[group.key] = expanded_vote.evidence[group.key]
264
+ replica_votes[group.key] = expanded_vote.replica_votes[group.key]
265
+
266
+ unresolved = [group.key for group in merged.groups if verdicts[group.key] is Verdict.UNCERTAIN]
267
+ return AdjudicationOutcome(
268
+ verdicts=verdicts,
269
+ reasons=reasons,
270
+ evidence=evidence,
271
+ replica_votes=replica_votes,
272
+ unresolved=unresolved,
273
+ coerced_rejections=coerced_rejections,
274
+ )
275
+
276
+
277
+ __all__ = [
278
+ "AdjudicationOutcome",
279
+ "adjudicate",
280
+ "adjudication_schema",
281
+ "build_adjudication_prompt",
282
+ ]