rvw 0.1.0__py3-none-any.whl
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/__init__.py +10 -0
- rvw/_version.py +3 -0
- rvw/adjudicate.py +282 -0
- rvw/cli.py +923 -0
- rvw/diffbudget.py +202 -0
- rvw/discover.py +193 -0
- rvw/dispatch.py +90 -0
- rvw/doctor.py +119 -0
- rvw/hunks.py +126 -0
- rvw/lane.py +83 -0
- rvw/merge.py +362 -0
- rvw/policy.py +167 -0
- rvw/prompts.py +60 -0
- rvw/publish.py +219 -0
- rvw/registry.py +75 -0
- rvw/report.py +331 -0
- rvw/runtimes/__init__.py +68 -0
- rvw/runtimes/codex.py +242 -0
- rvw/sample.py +133 -0
- rvw/schema.py +103 -0
- rvw/store.py +166 -0
- rvw/target.py +212 -0
- rvw-0.1.0.dist-info/METADATA +151 -0
- rvw-0.1.0.dist-info/RECORD +27 -0
- rvw-0.1.0.dist-info/WHEEL +4 -0
- rvw-0.1.0.dist-info/entry_points.txt +3 -0
- rvw-0.1.0.dist-info/licenses/LICENSE +21 -0
rvw/__init__.py
ADDED
|
@@ -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__"]
|
rvw/_version.py
ADDED
rvw/adjudicate.py
ADDED
|
@@ -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
|
+
]
|