autoforge-cli 0.1.0a0__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.
- autoforge/__init__.py +5 -0
- autoforge/agent_worktree.py +653 -0
- autoforge/certificate.py +495 -0
- autoforge/child_env.py +871 -0
- autoforge/claims.py +726 -0
- autoforge/claude_stream.py +501 -0
- autoforge/cli.py +2884 -0
- autoforge/config.py +2178 -0
- autoforge/config_fingerprint.py +374 -0
- autoforge/doctor.py +1551 -0
- autoforge/doctor_sandbox.py +691 -0
- autoforge/effect_ops.py +1272 -0
- autoforge/effects.py +3210 -0
- autoforge/engine.py +16637 -0
- autoforge/epic_commands.py +1486 -0
- autoforge/epic_graph.py +852 -0
- autoforge/errors.py +536 -0
- autoforge/evidence.py +1722 -0
- autoforge/executor.py +1601 -0
- autoforge/executor_duplex.py +985 -0
- autoforge/executor_supervised.py +144 -0
- autoforge/git_transport.py +970 -0
- autoforge/github.py +3402 -0
- autoforge/interaction.py +1027 -0
- autoforge/local_workspace.py +1253 -0
- autoforge/locking.py +1050 -0
- autoforge/loop_detect.py +660 -0
- autoforge/loop_guard.py +404 -0
- autoforge/materialize.py +722 -0
- autoforge/ownership.py +240 -0
- autoforge/pi_rpc.py +713 -0
- autoforge/premerge.py +181 -0
- autoforge/profiles.py +106 -0
- autoforge/progress.py +392 -0
- autoforge/prompts/__init__.py +173 -0
- autoforge/prompts/acceptance_actions.md +39 -0
- autoforge/prompts/analyze_execute.md +128 -0
- autoforge/prompts/common.md +132 -0
- autoforge/prompts/correction.md +12 -0
- autoforge/prompts/fix.md +252 -0
- autoforge/prompts/local_analyze_execute.md +69 -0
- autoforge/prompts/local_common.md +149 -0
- autoforge/prompts/local_fix.md +96 -0
- autoforge/prompts/local_review.md +103 -0
- autoforge/prompts/operator_acceptance.md +61 -0
- autoforge/prompts/replan_reexecute.md +991 -0
- autoforge/prompts/review.md +312 -0
- autoforge/prompts/update_epic.md +120 -0
- autoforge/prompts/update_epic_rerequest.md +85 -0
- autoforge/prompts/validation_evidence.md +26 -0
- autoforge/prompts/validation_failure.md +22 -0
- autoforge/prompts/worktree_state.md +45 -0
- autoforge/providers.py +2893 -0
- autoforge/read_context.py +389 -0
- autoforge/redaction.py +408 -0
- autoforge/replan.py +208 -0
- autoforge/replan_txn.py +1930 -0
- autoforge/result_parser.py +2234 -0
- autoforge/roadmap.py +151 -0
- autoforge/run_contract.py +408 -0
- autoforge/run_namespace.py +839 -0
- autoforge/runlog.py +764 -0
- autoforge/safefs.py +1717 -0
- autoforge/sandbox.py +1313 -0
- autoforge/sandbox_bwrap.py +815 -0
- autoforge/sandbox_pin.py +170 -0
- autoforge/sandbox_policy.py +1107 -0
- autoforge/scheduler.py +1143 -0
- autoforge/scheduler_state.py +2316 -0
- autoforge/scheduler_stop.py +421 -0
- autoforge/sdk_policy.py +988 -0
- autoforge/state.py +2417 -0
- autoforge/transitions.py +388 -0
- autoforge/validation.py +324 -0
- autoforge/worker.py +568 -0
- autoforge_cli-0.1.0a0.dist-info/METADATA +525 -0
- autoforge_cli-0.1.0a0.dist-info/RECORD +81 -0
- autoforge_cli-0.1.0a0.dist-info/WHEEL +5 -0
- autoforge_cli-0.1.0a0.dist-info/entry_points.txt +2 -0
- autoforge_cli-0.1.0a0.dist-info/licenses/LICENSE +21 -0
- autoforge_cli-0.1.0a0.dist-info/top_level.txt +1 -0
autoforge/certificate.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""The controller's review certificate: issuance and applicability (ADR 0006 D12, #174).
|
|
2
|
+
|
|
3
|
+
A ``ReviewCertificate`` means exactly (D1.2): for candidate C, in review
|
|
4
|
+
round N, bound to PR / HEAD / base / merge base (REMOTE) or to workspace
|
|
5
|
+
fingerprint F (LOCAL), the controller accepted a clean review result; it
|
|
6
|
+
presented exactly validation set V to that review, and every record in V is
|
|
7
|
+
``PASS`` for C under the definitions and policy recorded; for REMOTE, it
|
|
8
|
+
verified the published review comment identity K. It is not a GitHub
|
|
9
|
+
approval, a CI result, permission to merge, proof that the code is correct
|
|
10
|
+
or an authorization token, and it stands in for no merge-gate fact
|
|
11
|
+
(:data:`NOT_A_PERMISSION`).
|
|
12
|
+
|
|
13
|
+
Two things live here:
|
|
14
|
+
|
|
15
|
+
- **Issuance** (D12.2). :func:`remote_certificate` and
|
|
16
|
+
:func:`local_certificate` build the certificate from persisted, verified
|
|
17
|
+
inputs only: the ``review_evidence`` binding of the completed round (its
|
|
18
|
+
candidate, record seqs and digests, set and policy digests are copied,
|
|
19
|
+
never computed from the configuration in force), the review binding, the
|
|
20
|
+
clean verdict and, for REMOTE, the comment identity #162 observed. An input
|
|
21
|
+
that is missing or inconsistent raises :class:`CertificateIssuanceError`
|
|
22
|
+
naming it: no certificate is built.
|
|
23
|
+
- **Applicability** (D12.3). :func:`certificate_problem` says why a
|
|
24
|
+
certificate does not apply to the state, the configuration in force and,
|
|
25
|
+
when given, the live PR revision, or ``""`` when it does.
|
|
26
|
+
:func:`configuration_drift` is its set-and-policy part alone, which the
|
|
27
|
+
issuing write routes on (D12.6).
|
|
28
|
+
|
|
29
|
+
The schema, its bounds and its load-time integrity check are #172's
|
|
30
|
+
(:func:`autoforge.evidence.check_certificate`). A certificate is never shown
|
|
31
|
+
to an agent, rendered into a prompt or published, and never accepted from
|
|
32
|
+
one (D1.3). This module does no I/O and reads no clock: the engine passes
|
|
33
|
+
``issued_at``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from collections.abc import Mapping, Sequence
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
|
|
41
|
+
from .errors import AutoForgeError, ConfigurationError, StateError
|
|
42
|
+
from .evidence import (
|
|
43
|
+
KIND_LOCAL,
|
|
44
|
+
KIND_REMOTE,
|
|
45
|
+
SCHEMA,
|
|
46
|
+
Candidate,
|
|
47
|
+
Definition,
|
|
48
|
+
LocalCandidate,
|
|
49
|
+
Outcome,
|
|
50
|
+
Policy,
|
|
51
|
+
RecordView,
|
|
52
|
+
RemoteCandidate,
|
|
53
|
+
bound_records,
|
|
54
|
+
certificate_digest,
|
|
55
|
+
check_certificate,
|
|
56
|
+
eligible,
|
|
57
|
+
parse_candidate,
|
|
58
|
+
set_digest,
|
|
59
|
+
validation_set_digest,
|
|
60
|
+
)
|
|
61
|
+
from .state import AutoForgeState
|
|
62
|
+
from .transitions import WorkflowMode
|
|
63
|
+
from .validation import parse_comment_url, parse_pr_url
|
|
64
|
+
|
|
65
|
+
# The fixed wording every display of a certificate carries (D1.2).
|
|
66
|
+
NOT_A_PERMISSION = (
|
|
67
|
+
"a review certificate records what the controller observed of one clean review; it is "
|
|
68
|
+
"not a GitHub approval, a CI result or permission to merge, and the merge gate re-reads "
|
|
69
|
+
"every fact it decides on from GitHub"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
CLEAN = {"needs_fix_round": False, "actionable_findings": 0}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CertificateIssuanceError(AutoForgeError):
|
|
76
|
+
"""A certificate cannot be issued from the persisted inputs (ADR 0006 D12.2).
|
|
77
|
+
|
|
78
|
+
An invariant violation: the REVIEW entry never launches a reviewer under
|
|
79
|
+
another round's or candidate's binding (D5.5), a clean result over a
|
|
80
|
+
non-``PASS`` record is refused (D9.4) and load checks every record, so
|
|
81
|
+
the issuing write enters ``BLOCKED`` naming the input instead of the
|
|
82
|
+
transition, and writes no certificate.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class Configured:
|
|
88
|
+
"""The validation set and policy P the configuration in force yields (D3.5, D8.1)."""
|
|
89
|
+
|
|
90
|
+
definitions: tuple[Definition, ...]
|
|
91
|
+
policy: Policy
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def validation_set_digest(self) -> str:
|
|
95
|
+
return validation_set_digest(self.definitions)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def policy_digest(self) -> str:
|
|
99
|
+
return self.policy.digest
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class Revision:
|
|
104
|
+
"""A PR revision: the REMOTE review binding, or the one GitHub returned live."""
|
|
105
|
+
|
|
106
|
+
pr_url: str
|
|
107
|
+
head_sha: str
|
|
108
|
+
base_ref: str
|
|
109
|
+
merge_base_sha: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# -- issuance (D12.2) ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _issuance_inputs(
|
|
116
|
+
binding: Mapping[str, object],
|
|
117
|
+
records: Sequence[RecordView],
|
|
118
|
+
*,
|
|
119
|
+
review_round: int,
|
|
120
|
+
needs_fix_round: bool,
|
|
121
|
+
actionable_findings: int,
|
|
122
|
+
) -> tuple[Candidate, list[RecordView]]:
|
|
123
|
+
"""The binding's candidate and named records, once every issuance requirement holds."""
|
|
124
|
+
if needs_fix_round is not False or actionable_findings != 0:
|
|
125
|
+
raise CertificateIssuanceError(
|
|
126
|
+
f"review round {review_round} is not clean (needs_fix_round {needs_fix_round}, "
|
|
127
|
+
f"{actionable_findings} actionable finding(s)); only a clean result is certified"
|
|
128
|
+
)
|
|
129
|
+
if not binding:
|
|
130
|
+
raise CertificateIssuanceError(
|
|
131
|
+
f"no review_evidence binding is persisted for review round {review_round}: the "
|
|
132
|
+
"controller cannot say which validation the review was presented"
|
|
133
|
+
)
|
|
134
|
+
bound_round = binding.get("review_round")
|
|
135
|
+
if bound_round != review_round:
|
|
136
|
+
raise CertificateIssuanceError(
|
|
137
|
+
f"the review_evidence binding is of review round {bound_round}, not of the "
|
|
138
|
+
f"completed round {review_round}"
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
candidate = parse_candidate(binding.get("candidate"), "the review_evidence candidate")
|
|
142
|
+
named = bound_records(binding, records)
|
|
143
|
+
except StateError as exc:
|
|
144
|
+
raise CertificateIssuanceError(f"the review_evidence binding is unusable: {exc}") from None
|
|
145
|
+
for view in named:
|
|
146
|
+
if view.outcome is not Outcome.PASS:
|
|
147
|
+
raise CertificateIssuanceError(
|
|
148
|
+
f"the review_evidence binding names validation record #{view.seq} (definition "
|
|
149
|
+
f"[{view.index}]), which is {view.outcome.value}, not PASS"
|
|
150
|
+
)
|
|
151
|
+
if view.candidate.identity != candidate.identity:
|
|
152
|
+
raise CertificateIssuanceError(
|
|
153
|
+
f"the review_evidence binding names validation record #{view.seq}, which is "
|
|
154
|
+
"for another candidate"
|
|
155
|
+
)
|
|
156
|
+
if view.policy_digest != binding.get("policy_digest"):
|
|
157
|
+
raise CertificateIssuanceError(
|
|
158
|
+
f"the review_evidence binding names validation record #{view.seq}, which ran "
|
|
159
|
+
"under another policy"
|
|
160
|
+
)
|
|
161
|
+
if set_digest([v.definition_digest for v in named]) != binding.get("validation_set_digest"):
|
|
162
|
+
raise CertificateIssuanceError(
|
|
163
|
+
f"the {len(named)} validation record(s) the review_evidence binding names do not "
|
|
164
|
+
"encode to its validation_set_digest: the review was not presented a whole set"
|
|
165
|
+
)
|
|
166
|
+
return candidate, named
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _common(
|
|
170
|
+
kind: str,
|
|
171
|
+
candidate: Candidate,
|
|
172
|
+
review_round: int,
|
|
173
|
+
binding: Mapping[str, object],
|
|
174
|
+
issued_at: str,
|
|
175
|
+
) -> dict:
|
|
176
|
+
pairs = binding["records"]
|
|
177
|
+
assert isinstance(pairs, list) # bound_records read it
|
|
178
|
+
return {
|
|
179
|
+
"schema": SCHEMA,
|
|
180
|
+
"kind": kind,
|
|
181
|
+
"candidate": candidate.to_dict(),
|
|
182
|
+
"review_round": review_round,
|
|
183
|
+
"records": [{"seq": pair["seq"], "digest": pair["digest"]} for pair in pairs],
|
|
184
|
+
"validation_set_digest": binding["validation_set_digest"],
|
|
185
|
+
"policy_digest": binding["policy_digest"],
|
|
186
|
+
"clean": dict(CLEAN),
|
|
187
|
+
"issued_at": issued_at,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _sealed(
|
|
192
|
+
certificate: dict,
|
|
193
|
+
records: Sequence[RecordView],
|
|
194
|
+
*,
|
|
195
|
+
local: bool,
|
|
196
|
+
repository: str,
|
|
197
|
+
run_id: str,
|
|
198
|
+
) -> dict:
|
|
199
|
+
"""Digest ``certificate`` and check it as a load will: a writer never builds one it refuses."""
|
|
200
|
+
certificate["digest"] = certificate_digest(certificate)
|
|
201
|
+
try:
|
|
202
|
+
check_certificate(certificate, records, local=local, repository=repository, run_id=run_id)
|
|
203
|
+
except StateError as exc:
|
|
204
|
+
raise CertificateIssuanceError(f"the certificate would not load: {exc}") from None
|
|
205
|
+
return certificate
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def remote_certificate(
|
|
209
|
+
binding: Mapping[str, object],
|
|
210
|
+
records: Sequence[RecordView],
|
|
211
|
+
*,
|
|
212
|
+
repository: str,
|
|
213
|
+
run_id: str,
|
|
214
|
+
review_round: int,
|
|
215
|
+
needs_fix_round: bool,
|
|
216
|
+
actionable_findings: int,
|
|
217
|
+
reviewed: Revision,
|
|
218
|
+
comment_url: str,
|
|
219
|
+
marker_key: Mapping[str, object],
|
|
220
|
+
issued_at: str,
|
|
221
|
+
) -> dict:
|
|
222
|
+
"""The REMOTE certificate of a clean round (D12.1, D12.2).
|
|
223
|
+
|
|
224
|
+
``reviewed`` is the review binding the round's comment marker names
|
|
225
|
+
(PR, HEAD, base, merge base), ``comment_url`` the comment #162 read back
|
|
226
|
+
(or its journal observed) and ``marker_key`` that marker's key. The
|
|
227
|
+
binding's candidate must be the reviewed HEAD's commit: its tree was
|
|
228
|
+
derived from that commit at the REVIEW entry (#173).
|
|
229
|
+
"""
|
|
230
|
+
candidate, _ = _issuance_inputs(
|
|
231
|
+
binding,
|
|
232
|
+
records,
|
|
233
|
+
review_round=review_round,
|
|
234
|
+
needs_fix_round=needs_fix_round,
|
|
235
|
+
actionable_findings=actionable_findings,
|
|
236
|
+
)
|
|
237
|
+
if not isinstance(candidate, RemoteCandidate):
|
|
238
|
+
raise CertificateIssuanceError("the review_evidence binding names a LOCAL candidate")
|
|
239
|
+
if candidate.commit != reviewed.head_sha.lower():
|
|
240
|
+
raise CertificateIssuanceError(
|
|
241
|
+
f"the review_evidence binding names commit {candidate.commit[:12]}, not the "
|
|
242
|
+
f"reviewed HEAD {reviewed.head_sha[:12]}"
|
|
243
|
+
)
|
|
244
|
+
try:
|
|
245
|
+
comment = parse_comment_url(comment_url)
|
|
246
|
+
pr_url = parse_pr_url(reviewed.pr_url).canonical
|
|
247
|
+
except ConfigurationError as exc:
|
|
248
|
+
raise CertificateIssuanceError(f"the review comment identity is unusable: {exc}") from None
|
|
249
|
+
certificate = _common(KIND_REMOTE, candidate, review_round, binding, issued_at)
|
|
250
|
+
certificate.update(
|
|
251
|
+
{
|
|
252
|
+
"reviewed_pr_url": pr_url,
|
|
253
|
+
"reviewed_head_sha": reviewed.head_sha.lower(),
|
|
254
|
+
"reviewed_base_ref": reviewed.base_ref,
|
|
255
|
+
"reviewed_merge_base_sha": reviewed.merge_base_sha,
|
|
256
|
+
"comment": {
|
|
257
|
+
"url": comment.canonical,
|
|
258
|
+
"id": comment.comment_id,
|
|
259
|
+
"marker_key": {
|
|
260
|
+
"review_round": marker_key.get("review_round"),
|
|
261
|
+
"reviewed_head_sha": marker_key.get("reviewed_head_sha"),
|
|
262
|
+
"reviewed_base_ref": marker_key.get("reviewed_base_ref"),
|
|
263
|
+
"reviewed_merge_base_sha": marker_key.get("reviewed_merge_base_sha"),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
return _sealed(certificate, records, local=False, repository=repository, run_id=run_id)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def local_certificate(
|
|
272
|
+
binding: Mapping[str, object],
|
|
273
|
+
records: Sequence[RecordView],
|
|
274
|
+
*,
|
|
275
|
+
run_id: str,
|
|
276
|
+
review_round: int,
|
|
277
|
+
needs_fix_round: bool,
|
|
278
|
+
actionable_findings: int,
|
|
279
|
+
reviewed_workspace_fingerprint: str,
|
|
280
|
+
issued_at: str,
|
|
281
|
+
) -> dict:
|
|
282
|
+
"""The LOCAL certificate of a clean round (D12.1, D12.2, D12.5): GitHub is never consulted.
|
|
283
|
+
|
|
284
|
+
``reviewed_workspace_fingerprint`` is the fingerprint the round was
|
|
285
|
+
bound to and re-checked against after the reviewer exited; the
|
|
286
|
+
binding's candidate must be that workspace.
|
|
287
|
+
"""
|
|
288
|
+
candidate, _ = _issuance_inputs(
|
|
289
|
+
binding,
|
|
290
|
+
records,
|
|
291
|
+
review_round=review_round,
|
|
292
|
+
needs_fix_round=needs_fix_round,
|
|
293
|
+
actionable_findings=actionable_findings,
|
|
294
|
+
)
|
|
295
|
+
if not isinstance(candidate, LocalCandidate):
|
|
296
|
+
raise CertificateIssuanceError("the review_evidence binding names a REMOTE candidate")
|
|
297
|
+
if candidate.fingerprint != reviewed_workspace_fingerprint:
|
|
298
|
+
raise CertificateIssuanceError(
|
|
299
|
+
f"the review_evidence binding names workspace {candidate.fingerprint[:16]}..., not "
|
|
300
|
+
f"the reviewed workspace {reviewed_workspace_fingerprint[:16]}..."
|
|
301
|
+
)
|
|
302
|
+
certificate = _common(KIND_LOCAL, candidate, review_round, binding, issued_at)
|
|
303
|
+
certificate["reviewed_workspace_fingerprint"] = reviewed_workspace_fingerprint
|
|
304
|
+
return _sealed(certificate, records, local=True, repository="", run_id=run_id)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# -- applicability (D12.3, D12.6, D13.2) -------------------------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def configuration_drift(certificate: Mapping[str, object], configured: Configured) -> str:
|
|
311
|
+
"""Which of the certificate's set and policy digests the configuration no longer yields, or "".
|
|
312
|
+
|
|
313
|
+
D12.6: the issuing write routes on this, and so does
|
|
314
|
+
:func:`certificate_problem`, so that a drift is named as the drift.
|
|
315
|
+
"""
|
|
316
|
+
drifted = []
|
|
317
|
+
if certificate.get("validation_set_digest") != configured.validation_set_digest:
|
|
318
|
+
drifted.append(
|
|
319
|
+
f"the validation set changed (the review was presented set "
|
|
320
|
+
f"{str(certificate.get('validation_set_digest'))[:16]}..., the configuration now "
|
|
321
|
+
f"yields {configured.validation_set_digest[:16]}...)"
|
|
322
|
+
)
|
|
323
|
+
if certificate.get("policy_digest") != configured.policy_digest:
|
|
324
|
+
drifted.append(
|
|
325
|
+
f"the validation policy changed (the review was presented policy "
|
|
326
|
+
f"{str(certificate.get('policy_digest'))[:16]}..., the configuration now yields "
|
|
327
|
+
f"{configured.policy_digest[:16]}...)"
|
|
328
|
+
)
|
|
329
|
+
return "; ".join(drifted)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _binding_problem(certificate: Mapping[str, object], state: AutoForgeState) -> str:
|
|
333
|
+
"""REMOTE: the certificate's review binding and comment against the state's."""
|
|
334
|
+
for name in (
|
|
335
|
+
"reviewed_pr_url",
|
|
336
|
+
"reviewed_head_sha",
|
|
337
|
+
"reviewed_base_ref",
|
|
338
|
+
"reviewed_merge_base_sha",
|
|
339
|
+
):
|
|
340
|
+
held = getattr(state, name)
|
|
341
|
+
if name == "reviewed_head_sha":
|
|
342
|
+
held = held.lower()
|
|
343
|
+
if certificate.get(name) != held:
|
|
344
|
+
return (
|
|
345
|
+
f"its {name} {str(certificate.get(name))[:80]!r} is not the state's {held[:80]!r}"
|
|
346
|
+
)
|
|
347
|
+
candidate = certificate.get("candidate")
|
|
348
|
+
commit = candidate.get("commit") if isinstance(candidate, dict) else None
|
|
349
|
+
if commit != state.reviewed_head_sha.lower():
|
|
350
|
+
return f"its candidate commit {str(commit)[:12]} is not the reviewed HEAD"
|
|
351
|
+
comment = certificate.get("comment")
|
|
352
|
+
url = comment.get("url") if isinstance(comment, dict) else None
|
|
353
|
+
if url != state.last_review_comment_url:
|
|
354
|
+
return (
|
|
355
|
+
f"its review comment {str(url)[:200]} is not the last review comment "
|
|
356
|
+
f"{state.last_review_comment_url[:200] or '(none)'}"
|
|
357
|
+
)
|
|
358
|
+
return ""
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _local_binding_problem(certificate: Mapping[str, object], state: AutoForgeState) -> str:
|
|
362
|
+
"""LOCAL: the certificate's workspace against the one the state's last review bound."""
|
|
363
|
+
bound = state.reviewed_workspace_fingerprint
|
|
364
|
+
held = certificate.get("reviewed_workspace_fingerprint")
|
|
365
|
+
if held != bound:
|
|
366
|
+
return (
|
|
367
|
+
f"its reviewed workspace {str(held)[:16]}... is not the state's reviewed workspace "
|
|
368
|
+
f"{bound[:16] or '(none)'}..."
|
|
369
|
+
)
|
|
370
|
+
try:
|
|
371
|
+
policy_digest = state.local_contract().workspace_policy.digest()
|
|
372
|
+
except (StateError, ConfigurationError) as exc:
|
|
373
|
+
return f"the run contract is unreadable: {exc}"
|
|
374
|
+
expected = LocalCandidate(bound, policy_digest, state.run_id).identity
|
|
375
|
+
candidate = parse_candidate(certificate.get("candidate"), "the certificate candidate")
|
|
376
|
+
if candidate.identity != expected:
|
|
377
|
+
return "its candidate is not the reviewed workspace under this run's contract"
|
|
378
|
+
return ""
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def certificate_problem(
|
|
382
|
+
certificate: Mapping[str, object],
|
|
383
|
+
state: AutoForgeState,
|
|
384
|
+
configured: Configured,
|
|
385
|
+
live: Revision | None = None,
|
|
386
|
+
) -> str:
|
|
387
|
+
"""Why ``certificate`` does not apply, or ``""`` when it does (D12.3; #175 adds ``live``).
|
|
388
|
+
|
|
389
|
+
Checked, in this order, each failing with one bounded reason: it is
|
|
390
|
+
held, its kind is the run's mode, its digest verifies and it loads as
|
|
391
|
+
a certificate against the state's records (D11.1); its round is the
|
|
392
|
+
state's completed round, and that round's result is still ``clean`` (an
|
|
393
|
+
observed drift marks it ``stale``, #175); its review binding equals the
|
|
394
|
+
state's (REMOTE: PR, HEAD, base ref and merge base, its candidate's
|
|
395
|
+
commit is the reviewed HEAD and its comment is
|
|
396
|
+
``last_review_comment_url``; LOCAL: the reviewed workspace fingerprint,
|
|
397
|
+
under this run's contract); the
|
|
398
|
+
configuration in force yields its set and policy digests
|
|
399
|
+
(:func:`configuration_drift`); and every record it names is eligible
|
|
400
|
+
evidence for its candidate under that set and policy
|
|
401
|
+
(:func:`autoforge.evidence.eligible`). With ``live`` (REMOTE only: the
|
|
402
|
+
PR URL, HEAD, base ref and merge base as GitHub returned them) those
|
|
403
|
+
must equal the certificate's too. Pure: no I/O.
|
|
404
|
+
"""
|
|
405
|
+
if not certificate:
|
|
406
|
+
return "no review certificate is held"
|
|
407
|
+
local = state.mode == WorkflowMode.LOCAL
|
|
408
|
+
if live is not None and local:
|
|
409
|
+
raise ValueError("a LOCAL certificate has no live GitHub revision")
|
|
410
|
+
kind = certificate.get("kind")
|
|
411
|
+
mode_kind = KIND_LOCAL if local else KIND_REMOTE
|
|
412
|
+
if kind != mode_kind:
|
|
413
|
+
return f"it is a {str(kind)[:16]!r} certificate in a {mode_kind} run"
|
|
414
|
+
if certificate_digest(certificate) != certificate.get("digest"):
|
|
415
|
+
return "its digest does not verify: it was edited or corrupted"
|
|
416
|
+
try:
|
|
417
|
+
records = state.validation_evidence()
|
|
418
|
+
check_certificate(
|
|
419
|
+
certificate,
|
|
420
|
+
records,
|
|
421
|
+
local=local,
|
|
422
|
+
repository=state.repository,
|
|
423
|
+
run_id=state.run_id,
|
|
424
|
+
)
|
|
425
|
+
except StateError as exc:
|
|
426
|
+
return f"it does not load: {str(exc)[:300]}"
|
|
427
|
+
if certificate["review_round"] != state.review_round:
|
|
428
|
+
return (
|
|
429
|
+
f"it is of review round {certificate['review_round']}, not of the completed round "
|
|
430
|
+
f"{state.review_round}"
|
|
431
|
+
)
|
|
432
|
+
if state.last_review_result != "clean":
|
|
433
|
+
# #175: a drift the controller observed (a push, a retarget, a
|
|
434
|
+
# rewritten base) marks the round stale without moving its binding.
|
|
435
|
+
return (
|
|
436
|
+
f"the state's last review result is {state.last_review_result[:32] or 'unrecorded'!r}"
|
|
437
|
+
", not the clean round it certifies"
|
|
438
|
+
)
|
|
439
|
+
problem = (
|
|
440
|
+
_local_binding_problem(certificate, state)
|
|
441
|
+
if local
|
|
442
|
+
else _binding_problem(certificate, state)
|
|
443
|
+
)
|
|
444
|
+
if problem:
|
|
445
|
+
return problem
|
|
446
|
+
drift = configuration_drift(certificate, configured)
|
|
447
|
+
if drift:
|
|
448
|
+
return drift
|
|
449
|
+
candidate = parse_candidate(certificate["candidate"], "the certificate candidate")
|
|
450
|
+
named = bound_records(certificate, records)
|
|
451
|
+
for definition, view in zip(configured.definitions, named, strict=True):
|
|
452
|
+
if not eligible(
|
|
453
|
+
view, candidate, definition, configured.policy, state.validation_override_seq
|
|
454
|
+
):
|
|
455
|
+
return (
|
|
456
|
+
f"validation record #{view.seq} it names is not evidence for definition "
|
|
457
|
+
f"[{definition.index}] of its candidate under the policy in force"
|
|
458
|
+
)
|
|
459
|
+
if live is not None:
|
|
460
|
+
return _live_problem(certificate, live)
|
|
461
|
+
return ""
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _live_problem(certificate: Mapping[str, object], live: Revision) -> str:
|
|
465
|
+
try:
|
|
466
|
+
same_pr = parse_pr_url(live.pr_url).same_target(
|
|
467
|
+
parse_pr_url(str(certificate["reviewed_pr_url"]))
|
|
468
|
+
)
|
|
469
|
+
except ConfigurationError:
|
|
470
|
+
same_pr = False
|
|
471
|
+
if not same_pr:
|
|
472
|
+
return (
|
|
473
|
+
f"it is of PR {certificate['reviewed_pr_url']}, not of the live PR {live.pr_url[:200]}"
|
|
474
|
+
)
|
|
475
|
+
for name, value in (
|
|
476
|
+
("reviewed_head_sha", live.head_sha.lower()),
|
|
477
|
+
("reviewed_base_ref", live.base_ref),
|
|
478
|
+
("reviewed_merge_base_sha", live.merge_base_sha),
|
|
479
|
+
):
|
|
480
|
+
if certificate[name] != value:
|
|
481
|
+
return f"its {name} {str(certificate[name])[:80]!r} is not the live {value[:80]!r}"
|
|
482
|
+
return ""
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def describe(certificate: Mapping[str, object]) -> str:
|
|
486
|
+
"""One display line: kind, candidate, round, set and policy digests, ``issued_at``."""
|
|
487
|
+
candidate = parse_candidate(certificate["candidate"], "the certificate candidate")
|
|
488
|
+
pairs = certificate["records"]
|
|
489
|
+
count = len(pairs) if isinstance(pairs, list) else 0
|
|
490
|
+
return (
|
|
491
|
+
f"{certificate['kind']}, {candidate.describe()}, review round "
|
|
492
|
+
f"{certificate['review_round']}, {count} record(s) "
|
|
493
|
+
f"of set {str(certificate['validation_set_digest'])[:16]}..., policy "
|
|
494
|
+
f"{str(certificate['policy_digest'])[:16]}..., issued {certificate['issued_at']}"
|
|
495
|
+
)
|