codereview-loop 0.13.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.
codereview/claims.py ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """Builder-claim verification for the run record — Epic C4.
3
+
4
+ A builder's work report asserts things: "I ran the tests and
5
+ they passed." The panel cannot run anything, so until now those
6
+ assertions entered a verification round as the builder's word.
7
+ This module checks the checkable part of them — the commands
8
+ the report claims were run, with the outcomes it claims they
9
+ had — and records the result in the run record's
10
+ `claim_verification` section.
11
+
12
+ The machinery is the Epic C evidence runner's, composed, never
13
+ duplicated: `evidence.parse_claims` is the one parser of claim
14
+ forms (a backtick-quoted command with an outcome on the same
15
+ line, or a labelled `Ran:` / `Tests:` / `Command:` line), and
16
+ `evidence.verify_claims` re-runs each claim through the C5
17
+ sandbox (throwaway copy of the tree, allow-listed environment,
18
+ no network, timeouts, process-group kill). There is no second
19
+ parser and no second executor in this module; what it adds is
20
+ the record-facing contract around them:
21
+
22
+ - **Statuses.** Each extracted claim ends `verified` (the
23
+ re-run matches the claim), `mismatch` (the re-run contradicts
24
+ the claim — the claim is downgraded: recorded as not
25
+ standing, with claimed vs actual outcomes stated), or
26
+ `unverifiable` (the re-run could not be performed — the
27
+ sandbox itself failed). A line that is *shaped* like a claim
28
+ — it presents a command in backticks, or carries a claim
29
+ label — but does not parse to an unambiguous command +
30
+ outcome is recorded as `unparsed`: never guessed at, never
31
+ executed. Ordinary prose is not a claim attempt and is not
32
+ recorded at all.
33
+ - **Boundary, stated plainly.** Claim verification is evidence
34
+ recorded in the run record. It is NOT a gate input: no claim
35
+ status changes the verdict, a finding, or any other gate
36
+ input, and nothing in the gate reads this section. A mismatch
37
+ downgrades the *claim*, never the gate — the gate consumes
38
+ configured evidence through the C3 rule (`evidence.
39
+ gate_effect`) and the panel's findings, exactly as before.
40
+ The section's own `boundary` field repeats this in every
41
+ record that carries it.
42
+ - **Never raises.** A malformed report, a missing tree, a
43
+ sandbox failure — every failure is a recorded outcome
44
+ (`unverifiable`, with the reason), never an exception. A
45
+ run whose builder report cannot be claim-checked still
46
+ produces its record.
47
+
48
+ The standalone surface predates this module and keeps its own
49
+ vocabulary (`codereview evidence --claims report.md`, statuses
50
+ confirmed / downgraded / error); the mapping to this section's
51
+ vocabulary happens here, in one place.
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ from . import evidence as _evidence
57
+ from .core import redact_secrets
58
+
59
+ SCHEMA = "codereview.claims/1"
60
+
61
+ BOUNDARY = (
62
+ "Claim verification is evidence recorded in the run "
63
+ "record. It is not a gate input: no claim status changes "
64
+ "the verdict, a finding, or any other gate input. A "
65
+ "mismatch downgrades the claim, never the gate.")
66
+
67
+ # The evidence runner's statuses, in this section's vocabulary.
68
+ _STATUS_MAP = {"confirmed": "verified", "downgraded": "mismatch",
69
+ "error": "unverifiable"}
70
+
71
+ STATUSES = ("verified", "mismatch", "unverifiable", "unparsed")
72
+
73
+ _UNPARSED_TEXT_CHARS = 200
74
+
75
+
76
+ def _unparsed_entries(text, parsed_lines):
77
+ """Claim-shaped lines the parser did not extract, as
78
+ `unparsed` entries. A line is claim-shaped when it presents
79
+ a command in a backtick span or carries one of the claim
80
+ labels — the two forms `evidence.parse_claims` recognises.
81
+ The span form is checked first, mirroring the parser's own
82
+ precedence. Everything else is prose, not a claim attempt,
83
+ and is left unrecorded."""
84
+ entries = []
85
+ for lineno, line in enumerate(text.splitlines(), start=1):
86
+ if lineno in parsed_lines:
87
+ continue
88
+ if _evidence._SPAN_RE.search(line):
89
+ reason = ("command presented without an unambiguous "
90
+ "claimed outcome — not extracted, not run")
91
+ elif _evidence._CLAIM_LABEL_RE.match(line):
92
+ reason = ("labelled line did not yield an "
93
+ "unambiguous command and outcome — not "
94
+ "extracted, not run")
95
+ else:
96
+ continue
97
+ entries.append({
98
+ "line": lineno,
99
+ "status": "unparsed",
100
+ "text": redact_secrets(line.strip())[
101
+ :_UNPARSED_TEXT_CHARS],
102
+ "reason": reason})
103
+ return entries
104
+
105
+
106
+ def verify_report_claims(report_text, source_dir, timeout=None,
107
+ environ=None):
108
+ """Verify a builder work report's claimed commands against
109
+ `source_dir` and return the run record's
110
+ `claim_verification` section — or None when there is no
111
+ report to check (None / empty / whitespace-only), so a run
112
+ without a builder report carries no section at all.
113
+
114
+ Never raises. `timeout` is per claimed command and defaults
115
+ to the evidence runner's own bound — one timeout discipline
116
+ for every command the sandbox executes (C5)."""
117
+ if report_text is None:
118
+ return None
119
+ if isinstance(report_text, bytes):
120
+ report_text = report_text.decode("utf-8", "replace")
121
+ if not isinstance(report_text, str):
122
+ return {"schema": SCHEMA, "boundary": BOUNDARY,
123
+ "claims": [],
124
+ "summary": {"total": 0, "verified": 0,
125
+ "mismatch": 0, "unverifiable": 0,
126
+ "unparsed": 0},
127
+ "detail": "builder report was not text "
128
+ f"({type(report_text).__name__}); "
129
+ "no claims could be checked"}
130
+ if not report_text.strip():
131
+ return None
132
+ if timeout is None:
133
+ timeout = _evidence.DEFAULT_TIMEOUT
134
+ try:
135
+ parsed = _evidence.parse_claims(report_text)
136
+ except Exception: # noqa: BLE001 — recorded below, not raised
137
+ parsed = []
138
+ entries = []
139
+ try:
140
+ # P2-07: hand over the claims parsed above — the
141
+ # report text is parsed exactly once per verification.
142
+ result = _evidence.verify_claims(
143
+ report_text, source_dir, timeout=timeout,
144
+ environ=environ, claims=parsed)
145
+ for claim in result.get("claims") or []:
146
+ entries.append({
147
+ "line": claim.get("line"),
148
+ "command": claim.get("command"),
149
+ "claimed": claim.get("claimed"),
150
+ "claimed_exit_code": claim.get(
151
+ "claimed_exit_code"),
152
+ "status": _STATUS_MAP.get(claim.get("status"),
153
+ "unverifiable"),
154
+ "actual_exit_code": claim.get("actual_exit_code"),
155
+ "duration_s": claim.get("duration_s"),
156
+ "output_sha256": claim.get("output_sha256"),
157
+ "detail": claim.get("detail") or ""})
158
+ except Exception as exc: # noqa: BLE001 — a verification that
159
+ # could not run at all leaves every extracted claim
160
+ # unverifiable, with the reason, and the run continues.
161
+ for claim in parsed:
162
+ entries.append({
163
+ "line": claim.get("line"),
164
+ "command": claim.get("command"),
165
+ "claimed": claim.get("claimed"),
166
+ "claimed_exit_code": claim.get(
167
+ "claimed_exit_code"),
168
+ "status": "unverifiable",
169
+ "actual_exit_code": None,
170
+ "duration_s": None,
171
+ "output_sha256": None,
172
+ "detail": f"claim verification could not run: "
173
+ f"{exc}"})
174
+ parsed_lines = {c.get("line") for c in parsed}
175
+ entries.extend(_unparsed_entries(report_text, parsed_lines))
176
+ entries.sort(key=lambda e: e.get("line") or 0)
177
+ summary = {"total": len(entries)}
178
+ for status in STATUSES:
179
+ summary[status] = sum(1 for e in entries
180
+ if e["status"] == status)
181
+ return {"schema": SCHEMA, "boundary": BOUNDARY,
182
+ "claims": entries, "summary": summary}