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 +21 -0
- rvw-0.1.0/PKG-INFO +151 -0
- rvw-0.1.0/README.md +116 -0
- rvw-0.1.0/pyproject.toml +90 -0
- rvw-0.1.0/src/rvw/__init__.py +10 -0
- rvw-0.1.0/src/rvw/_version.py +3 -0
- rvw-0.1.0/src/rvw/adjudicate.py +282 -0
- rvw-0.1.0/src/rvw/cli.py +923 -0
- rvw-0.1.0/src/rvw/diffbudget.py +202 -0
- rvw-0.1.0/src/rvw/discover.py +193 -0
- rvw-0.1.0/src/rvw/dispatch.py +90 -0
- rvw-0.1.0/src/rvw/doctor.py +119 -0
- rvw-0.1.0/src/rvw/hunks.py +126 -0
- rvw-0.1.0/src/rvw/lane.py +83 -0
- rvw-0.1.0/src/rvw/merge.py +362 -0
- rvw-0.1.0/src/rvw/policy.py +167 -0
- rvw-0.1.0/src/rvw/prompts.py +60 -0
- rvw-0.1.0/src/rvw/publish.py +219 -0
- rvw-0.1.0/src/rvw/registry.py +75 -0
- rvw-0.1.0/src/rvw/report.py +331 -0
- rvw-0.1.0/src/rvw/runtimes/__init__.py +68 -0
- rvw-0.1.0/src/rvw/runtimes/codex.py +242 -0
- rvw-0.1.0/src/rvw/sample.py +133 -0
- rvw-0.1.0/src/rvw/schema.py +103 -0
- rvw-0.1.0/src/rvw/store.py +166 -0
- rvw-0.1.0/src/rvw/target.py +212 -0
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
|
+
[](https://github.com/Soju06/rvw/actions/workflows/ci.yml)
|
|
39
|
+
[](https://pypi.org/project/rvw/)
|
|
40
|
+
[](https://pypi.org/project/rvw/)
|
|
41
|
+
[](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
|
+
[](https://github.com/Soju06/rvw/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/rvw/)
|
|
5
|
+
[](https://pypi.org/project/rvw/)
|
|
6
|
+
[](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)
|
rvw-0.1.0/pyproject.toml
ADDED
|
@@ -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,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
|
+
]
|