repro-engine 1.2.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.
- repro/__init__.py +8 -0
- repro/__main__.py +3 -0
- repro/agents/__init__.py +67 -0
- repro/agents/backend.py +129 -0
- repro/agents/bias.py +109 -0
- repro/agents/cassette.py +413 -0
- repro/agents/cost.py +66 -0
- repro/agents/dispatch.py +148 -0
- repro/agents/events.py +165 -0
- repro/agents/evidence.py +254 -0
- repro/agents/gap.py +181 -0
- repro/agents/lockdown.py +131 -0
- repro/agents/openai_compat.py +474 -0
- repro/agents/prober.py +173 -0
- repro/agents/prompts/__init__.py +143 -0
- repro/agents/prompts/gap.md +20 -0
- repro/agents/prompts/lock.json +7 -0
- repro/agents/prompts/probe.md +22 -0
- repro/agents/prompts/qa.md +24 -0
- repro/agents/prompts/reader.md +28 -0
- repro/agents/prompts/reviewer.md +34 -0
- repro/agents/qa.py +173 -0
- repro/agents/reader.py +167 -0
- repro/agents/reviewer.py +191 -0
- repro/agents/sdk.py +236 -0
- repro/agents/setup.py +117 -0
- repro/agents/sources.py +102 -0
- repro/agents/stages.py +539 -0
- repro/agents/tools.py +231 -0
- repro/agents/untrusted.py +48 -0
- repro/agents/validate.py +169 -0
- repro/analysis/__init__.py +34 -0
- repro/analysis/_grimp_worker.py +66 -0
- repro/analysis/api.py +275 -0
- repro/analysis/blast.py +110 -0
- repro/analysis/changes.py +315 -0
- repro/analysis/imports.py +218 -0
- repro/analysis/modules.py +59 -0
- repro/analysis/static.py +54 -0
- repro/analysis/symbols.py +192 -0
- repro/api/__init__.py +5 -0
- repro/api/app.py +212 -0
- repro/api/cockpit/assets/JetBrainsMono-OFL.txt +93 -0
- repro/api/cockpit/assets/atlas.worker-CP1cHLLd.js +1 -0
- repro/api/cockpit/assets/demo-run-C9SGu-Og.jsonl +2334 -0
- repro/api/cockpit/assets/fleet-Bn3DZ83b.css +1 -0
- repro/api/cockpit/assets/fleet-C5arwxhB.js +1 -0
- repro/api/cockpit/assets/force-DkXbt76I.js +1 -0
- repro/api/cockpit/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- repro/api/cockpit/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- repro/api/cockpit/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- repro/api/cockpit/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- repro/api/cockpit/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- repro/api/cockpit/assets/jsxRuntime-B9Idad31.js +2 -0
- repro/api/cockpit/assets/jsxRuntime-DHHyguPz.css +1 -0
- repro/api/cockpit/assets/layout.worker-DLYbolgN.js +1 -0
- repro/api/cockpit/assets/main-BbRvaUOf.css +1 -0
- repro/api/cockpit/assets/main-CpPW1Zab.js +4376 -0
- repro/api/cockpit/assets/stress-run-TCZCEJwc.jsonl +18542 -0
- repro/api/cockpit/fleet.html +15 -0
- repro/api/cockpit/index.html +15 -0
- repro/api/stream.py +195 -0
- repro/api/token.py +48 -0
- repro/api/ui.py +17 -0
- repro/bus.py +150 -0
- repro/card/__init__.py +30 -0
- repro/card/assemble.py +224 -0
- repro/card/exports.py +329 -0
- repro/card/job.py +76 -0
- repro/card/markdown.py +175 -0
- repro/cli.py +923 -0
- repro/clock.py +15 -0
- repro/config/__init__.py +25 -0
- repro/config/settings.py +297 -0
- repro/doctor.py +461 -0
- repro/gitops/__init__.py +36 -0
- repro/gitops/changes.py +186 -0
- repro/gitops/git.py +121 -0
- repro/gitops/snapshot.py +398 -0
- repro/gitops/worktree.py +94 -0
- repro/ids.py +85 -0
- repro/integrations/__init__.py +1 -0
- repro/integrations/github/__init__.py +72 -0
- repro/integrations/github/action.py +183 -0
- repro/integrations/github/api.py +190 -0
- repro/integrations/github/mirror.py +104 -0
- repro/integrations/github/pr.py +127 -0
- repro/integrations/github/report.py +256 -0
- repro/integrations/mcp_server.py +224 -0
- repro/jsts.py +271 -0
- repro/logs.py +144 -0
- repro/orchestrator/__init__.py +43 -0
- repro/orchestrator/jobs.py +207 -0
- repro/orchestrator/runner.py +449 -0
- repro/paths.py +101 -0
- repro/pipeline.py +288 -0
- repro/policy/__init__.py +23 -0
- repro/policy/gap.py +173 -0
- repro/policy/invariants.py +379 -0
- repro/policy/loader.py +330 -0
- repro/policy/repro-yml.schema.json +100 -0
- repro/policy/stage.py +160 -0
- repro/policy/status.py +263 -0
- repro/provision/__init__.py +32 -0
- repro/provision/deps.py +274 -0
- repro/provision/env.py +541 -0
- repro/provision/project.py +416 -0
- repro/py.typed +0 -0
- repro/readable.py +73 -0
- repro/run_stage.py +1024 -0
- repro/runner/__init__.py +25 -0
- repro/runner/behaviour.py +341 -0
- repro/runner/observations.py +648 -0
- repro/runner/outcomes.py +193 -0
- repro/runner/perf.py +358 -0
- repro/runner/probes.py +681 -0
- repro/runner/pytest.py +549 -0
- repro/runner/pytest_config.py +125 -0
- repro/runner/selection.py +360 -0
- repro/runner/sides.py +63 -0
- repro/runner/testdiff.py +262 -0
- repro/runner/trace.py +195 -0
- repro/runner/twosided.py +435 -0
- repro/sandbox/__init__.py +30 -0
- repro/sandbox/_hook/repro_encode.py +734 -0
- repro/sandbox/_hook/repro_probe.py +77 -0
- repro/sandbox/_hook/repro_pytest.py +459 -0
- repro/sandbox/_hook/repro_trace.py +333 -0
- repro/sandbox/_hook/sitecustomize.py +199 -0
- repro/sandbox/_rlimits.py +55 -0
- repro/sandbox/detect.py +105 -0
- repro/sandbox/docker.py +462 -0
- repro/sandbox/evidence.py +68 -0
- repro/sandbox/image/Dockerfile +20 -0
- repro/sandbox/image.py +44 -0
- repro/sandbox/limits.py +33 -0
- repro/sandbox/process.py +336 -0
- repro/sandbox/winjob.py +242 -0
- repro/scrub.py +86 -0
- repro/store/__init__.py +24 -0
- repro/store/artifacts.py +145 -0
- repro/store/cbor.py +191 -0
- repro/store/db.py +118 -0
- repro/store/reader.py +132 -0
- repro/store/schema.py +165 -0
- repro/store/writer.py +452 -0
- repro/symbolic.py +288 -0
- repro_engine-1.2.0.dist-info/METADATA +196 -0
- repro_engine-1.2.0.dist-info/RECORD +151 -0
- repro_engine-1.2.0.dist-info/WHEEL +4 -0
- repro_engine-1.2.0.dist-info/entry_points.txt +3 -0
repro/__init__.py
ADDED
repro/__main__.py
ADDED
repro/agents/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Reviewer agents: the LLM backend, its lockdown and dispatch (Stage G; ADR-0007).
|
|
2
|
+
|
|
3
|
+
``LLMBackend`` is the one way the engine reaches a model; ``AgentSdkBackend`` is its default,
|
|
4
|
+
the Claude Agent SDK under the lockdown in ``lockdown``; ``OpenAICompatibleBackend`` reaches
|
|
5
|
+
Groq or a local server (ADR-0016); and ``CassetteBackend`` records calls and replays them
|
|
6
|
+
offline. Prompts are versioned files (``prompts``). ``run_agent`` runs a call in one of a run's
|
|
7
|
+
dispatch slots, within its ``RunLedger``, and reports it as events.
|
|
8
|
+
``EvidenceTools`` are what agents read: the run's evidence and the change's files (G03).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from repro.agents.backend import (
|
|
12
|
+
AgentRequest,
|
|
13
|
+
AgentResult,
|
|
14
|
+
Budget,
|
|
15
|
+
LLMBackend,
|
|
16
|
+
OnProgress,
|
|
17
|
+
Outcome,
|
|
18
|
+
Progress,
|
|
19
|
+
ReproTool,
|
|
20
|
+
Role,
|
|
21
|
+
Usage,
|
|
22
|
+
)
|
|
23
|
+
from repro.agents.cassette import (
|
|
24
|
+
CassetteBackend,
|
|
25
|
+
CassetteError,
|
|
26
|
+
MissingCassetteError,
|
|
27
|
+
StaleCassetteError,
|
|
28
|
+
)
|
|
29
|
+
from repro.agents.cost import RunLedger
|
|
30
|
+
from repro.agents.dispatch import SLOTS, Dispatch, concurrency_for, default_concurrency
|
|
31
|
+
from repro.agents.events import run_agent
|
|
32
|
+
from repro.agents.evidence import EvidenceView
|
|
33
|
+
from repro.agents.openai_compat import Endpoint, OpenAICompatibleBackend
|
|
34
|
+
from repro.agents.sdk import AgentSdkBackend
|
|
35
|
+
from repro.agents.sources import RepoSources, SourceError
|
|
36
|
+
from repro.agents.tools import EvidenceTools, ToolInputError
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"SLOTS",
|
|
40
|
+
"AgentRequest",
|
|
41
|
+
"AgentResult",
|
|
42
|
+
"AgentSdkBackend",
|
|
43
|
+
"Budget",
|
|
44
|
+
"CassetteBackend",
|
|
45
|
+
"CassetteError",
|
|
46
|
+
"Dispatch",
|
|
47
|
+
"Endpoint",
|
|
48
|
+
"EvidenceTools",
|
|
49
|
+
"EvidenceView",
|
|
50
|
+
"LLMBackend",
|
|
51
|
+
"MissingCassetteError",
|
|
52
|
+
"OnProgress",
|
|
53
|
+
"OpenAICompatibleBackend",
|
|
54
|
+
"Outcome",
|
|
55
|
+
"Progress",
|
|
56
|
+
"RepoSources",
|
|
57
|
+
"ReproTool",
|
|
58
|
+
"Role",
|
|
59
|
+
"RunLedger",
|
|
60
|
+
"SourceError",
|
|
61
|
+
"StaleCassetteError",
|
|
62
|
+
"ToolInputError",
|
|
63
|
+
"Usage",
|
|
64
|
+
"concurrency_for",
|
|
65
|
+
"default_concurrency",
|
|
66
|
+
"run_agent",
|
|
67
|
+
]
|
repro/agents/backend.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The interface every LLM call goes through (G01; ADR-0007, ARCHITECTURE §10.5).
|
|
2
|
+
|
|
3
|
+
An agent call is a request (a role, a system prompt, the user's text, the read-only tools it
|
|
4
|
+
may call, an optional output schema, a model, the prompt's version and a budget) and a result
|
|
5
|
+
(the structured output or text, how the run ended, its usage). The default implementation is
|
|
6
|
+
the Claude Agent SDK (``repro.agents.sdk``); cassettes (``repro.agents.cassette``) and tests
|
|
7
|
+
implement the same protocol, so the stages that use agents never depend on how the model is
|
|
8
|
+
reached.
|
|
9
|
+
|
|
10
|
+
``on_progress`` reports what the agent is doing while it runs (thinking, a tool call): the
|
|
11
|
+
``agent.state`` events the cockpit's dispatch and workspace panels draw (``repro.agents.events``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Final, Literal, Protocol
|
|
18
|
+
|
|
19
|
+
Role = Literal["read", "build", "review", "gap", "probe", "qa"]
|
|
20
|
+
#: How a run ended: ``ok`` with its output; or why there is none.
|
|
21
|
+
Outcome = Literal["ok", "max_turns", "max_budget", "timeout", "invalid_output", "error"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class Budget:
|
|
26
|
+
"""What one agent call may spend; every call has one (ADR-0007)."""
|
|
27
|
+
|
|
28
|
+
max_turns: int = 8
|
|
29
|
+
max_budget_usd: float = 1.0
|
|
30
|
+
timeout_s: float = 600.0
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
if self.max_turns < 1 or self.max_budget_usd <= 0 or self.timeout_s <= 0:
|
|
34
|
+
raise ValueError("a budget's limits must be positive")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#: A tool's name: words of lowercase letters and digits joined by single underscores.
|
|
38
|
+
TOOL_NAME: Final = re.compile(r"(?=.{1,48}$)[a-z][a-z0-9]*(?:_[a-z0-9]+)*")
|
|
39
|
+
#: A prompt's version: its file's name and version number, as ``reviewer/3`` (G02).
|
|
40
|
+
PROMPT_VERSION: Final = re.compile(r"[a-z][a-z0-9_-]{0,47}/[1-9][0-9]{0,5}")
|
|
41
|
+
#: A tool handler: its validated input, then the text it returns to the model.
|
|
42
|
+
ToolHandler = Callable[[Mapping[str, Any]], Awaitable[str]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class ReproTool:
|
|
47
|
+
"""A read-only, in-process tool the agent may call (``mcp__repro__<name>``)."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
description: str
|
|
51
|
+
input_schema: Mapping[str, Any]
|
|
52
|
+
handler: ToolHandler
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
if not TOOL_NAME.fullmatch(self.name):
|
|
56
|
+
raise ValueError(f"tool names are lower_snake_case, up to 48 characters: {self.name!r}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class AgentRequest:
|
|
61
|
+
role: Role
|
|
62
|
+
system_prompt: str
|
|
63
|
+
prompt: str
|
|
64
|
+
model: str
|
|
65
|
+
#: The version of the prompt the call was built from (``repro.agents.prompts``).
|
|
66
|
+
prompt_version: str
|
|
67
|
+
budget: Budget = field(default_factory=Budget)
|
|
68
|
+
tools: Sequence[ReproTool] = ()
|
|
69
|
+
#: JSON Schema (draft-07, ``$ref``-free) the final answer must satisfy, or None for text.
|
|
70
|
+
output_schema: Mapping[str, Any] | None = None
|
|
71
|
+
effort: Literal["low", "medium", "high", "xhigh", "max"] | None = None
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
if not PROMPT_VERSION.fullmatch(self.prompt_version):
|
|
75
|
+
raise ValueError(f"a prompt version is name/number: {self.prompt_version!r}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class Usage:
|
|
80
|
+
"""Client-side estimates, never billing data (research/04 §1.8)."""
|
|
81
|
+
|
|
82
|
+
input_tokens: int = 0
|
|
83
|
+
output_tokens: int = 0
|
|
84
|
+
cache_read_tokens: int = 0
|
|
85
|
+
cost_usd: float = 0.0
|
|
86
|
+
#: Whether ``cost_usd`` rests on a known price; False: the tokens are counted, the cost is
|
|
87
|
+
#: unknown (0 here, and no ``cost.estimate``; G10, ADR-0016).
|
|
88
|
+
priced: bool = True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class AgentResult:
|
|
93
|
+
outcome: Outcome
|
|
94
|
+
#: The schema-valid answer (with an ``output_schema``), else None.
|
|
95
|
+
structured: Any = None
|
|
96
|
+
text: str = ""
|
|
97
|
+
turns: int = 0
|
|
98
|
+
usage: Usage = field(default_factory=Usage)
|
|
99
|
+
#: Why there is no output, in a line (empty when ``ok``).
|
|
100
|
+
detail: str = ""
|
|
101
|
+
#: Tool calls the model made, in order: (tool name, whether it was allowed to run).
|
|
102
|
+
tool_calls: tuple[tuple[str, bool], ...] = ()
|
|
103
|
+
#: ``usage`` per model the call used (subagents included, research/04 §1.8).
|
|
104
|
+
by_model: Mapping[str, Usage] = field(default_factory=dict)
|
|
105
|
+
#: Served from a cassette: ``usage`` is what the call cost when it was recorded.
|
|
106
|
+
replayed: bool = False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True, slots=True)
|
|
110
|
+
class Progress:
|
|
111
|
+
"""What an agent is doing now (``agent.state``'s ``thinking`` and ``tool_call``)."""
|
|
112
|
+
|
|
113
|
+
state: Literal["thinking", "tool_call"]
|
|
114
|
+
#: The model turn it happened in, from 1 (a turn is one model response).
|
|
115
|
+
turn: int
|
|
116
|
+
tool: str | None = None
|
|
117
|
+
detail: str | None = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
OnProgress = Callable[[Progress], None]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class LLMBackend(Protocol):
|
|
124
|
+
"""Runs one agent call to its end; never raises for what the model does (the result says
|
|
125
|
+
so), only for a backend that cannot run at all."""
|
|
126
|
+
|
|
127
|
+
async def run(
|
|
128
|
+
self, request: AgentRequest, on_progress: OnProgress | None = None
|
|
129
|
+
) -> AgentResult: ...
|
repro/agents/bias.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Bias controls for the reviewers' verdicts (G08; ARCHITECTURE §10.4, research/04 §2.3).
|
|
2
|
+
|
|
3
|
+
- **Order swap.** LLM judges favour a position: in MT-Bench, GPT-4's verdicts survived swapping
|
|
4
|
+
the order of what it compared in 65 % of cases. A ``contradicted`` verdict flags the change,
|
|
5
|
+
so the reviewer is asked again with the evidence and the other claims in the opposite order
|
|
6
|
+
(``repro.agents.reviewer``'s ``reverse``); unless both orders give it, the verdict degrades
|
|
7
|
+
to ``unverifiable`` (``degraded``). The ``agent_order_swap`` setting can ask for the second
|
|
8
|
+
order on every verdict, which is how the flip rate is measured, or not at all.
|
|
9
|
+
- **Panel.** With ``agent_panel``, three reviewers judge each claim (their models in the
|
|
10
|
+
setting's order, the second with the order reversed) and a verdict stands when two of them
|
|
11
|
+
give it (``majority``); otherwise it is ``unverifiable``.
|
|
12
|
+
- **Judge ≠ author.** Judges favour their own generations (Panickssery et al.). A change's
|
|
13
|
+
commits and PR text often name the model that wrote it, as Claude Code's
|
|
14
|
+
``Co-Authored-By: Claude …`` trailer does; when every reviewer is of that model's family,
|
|
15
|
+
the card says so (``judge_is_author``). The families are read from model names only
|
|
16
|
+
(``family``), never from people's names or addresses.
|
|
17
|
+
|
|
18
|
+
Every verdict records how many judgements it rests on and how many of them gave it
|
|
19
|
+
(``agreement``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from collections import Counter
|
|
24
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
25
|
+
from typing import Any, Final
|
|
26
|
+
|
|
27
|
+
#: Model-name tokens and the family (the developer of the model) each belongs to.
|
|
28
|
+
FAMILIES: Final[tuple[tuple[str, str], ...]] = (
|
|
29
|
+
("claude", "anthropic"),
|
|
30
|
+
("gpt", "openai"),
|
|
31
|
+
("codex", "openai"),
|
|
32
|
+
("chatgpt", "openai"),
|
|
33
|
+
("gemini", "google"),
|
|
34
|
+
("qwen", "alibaba"),
|
|
35
|
+
("llama", "meta"),
|
|
36
|
+
("deepseek", "deepseek"),
|
|
37
|
+
("mistral", "mistral"),
|
|
38
|
+
)
|
|
39
|
+
#: The lines that name a change's author: a co-author trailer, a "generated with" line.
|
|
40
|
+
_AUTHOR_LINE: Final = re.compile(
|
|
41
|
+
r"^\s*(?:co-authored-by:|.*\bgenerated (?:with|by)\b)(?P<who>.*)$", re.IGNORECASE | re.MULTILINE
|
|
42
|
+
)
|
|
43
|
+
MAX_RATIONALE: Final = 4000
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def family(model: str) -> str | None:
|
|
47
|
+
"""The family a model name belongs to (``openai/gpt-oss-120b``: openai), if known."""
|
|
48
|
+
words = re.split(r"[^a-z0-9]+", model.lower())
|
|
49
|
+
for token, name in FAMILIES: # the token, or the token and a version (qwen3, gpt4o)
|
|
50
|
+
if any(re.fullmatch(rf"{token}(?:\d[\da-z]*)?", word) for word in words):
|
|
51
|
+
return name
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def author_families(texts: Iterable[str]) -> set[str]:
|
|
56
|
+
"""The model families the texts name as the change's authors (co-author trailers and
|
|
57
|
+
"generated with" lines only)."""
|
|
58
|
+
found = set()
|
|
59
|
+
for text in texts:
|
|
60
|
+
for match in _AUTHOR_LINE.finditer(text):
|
|
61
|
+
named = family(match.group("who"))
|
|
62
|
+
if named is not None:
|
|
63
|
+
found.add(named)
|
|
64
|
+
return found
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def judge_is_author(models: Sequence[str], authors: set[str]) -> str | None:
|
|
68
|
+
"""The family every judge shares with the change's authors, or None."""
|
|
69
|
+
families = {family(model) for model in models}
|
|
70
|
+
if len(families) == 1:
|
|
71
|
+
(shared,) = families
|
|
72
|
+
if shared is not None and shared in authors:
|
|
73
|
+
return shared
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def agreeing(verdict: Mapping[str, Any], runs: int, agree: int) -> dict[str, Any]:
|
|
78
|
+
"""``verdict`` with how many judgements it rests on and how many of them gave it."""
|
|
79
|
+
return {**verdict, "agreement": {"runs": runs, "agreeing": agree}}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def degraded(first: Mapping[str, Any], why: str, runs: int, agree: int) -> dict[str, Any]:
|
|
83
|
+
"""``first`` kept as ``unverifiable``: its judges did not agree (``why``). It still cites
|
|
84
|
+
and quotes what the first judgement did."""
|
|
85
|
+
rationale = f"Not kept as {first['verdict']}: {why}. The first judgement: {first['rationale']}"
|
|
86
|
+
return {
|
|
87
|
+
**first,
|
|
88
|
+
"verdict": "unverifiable",
|
|
89
|
+
"rationale": rationale[:MAX_RATIONALE],
|
|
90
|
+
"agreement": {"runs": runs, "agreeing": agree},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def majority(verdicts: Sequence[Mapping[str, Any]], runs: int) -> dict[str, Any] | None:
|
|
95
|
+
"""The first verdict whose value at least two of ``verdicts`` give, with its agreement;
|
|
96
|
+
None when no two agree."""
|
|
97
|
+
counts = Counter(v["verdict"] for v in verdicts)
|
|
98
|
+
for verdict in verdicts:
|
|
99
|
+
if counts[verdict["verdict"]] >= 2: # noqa: PLR2004 - two of three
|
|
100
|
+
return agreeing(verdict, runs, counts[verdict["verdict"]])
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def described(verdicts: Sequence[tuple[str, Mapping[str, Any] | str]]) -> str:
|
|
105
|
+
"""What each judge answered, for a rationale: ``m1: supported; m2: rejected (…)``."""
|
|
106
|
+
return "; ".join(
|
|
107
|
+
f"{model}: {answer['verdict'] if isinstance(answer, Mapping) else answer}"
|
|
108
|
+
for model, answer in verdicts
|
|
109
|
+
)
|