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 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
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the rvw version string."""
2
+
3
+ __version__ = "0.1.0"
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
+ ]