patchrail 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.
- patchrail/__init__.py +7 -0
- patchrail/__main__.py +7 -0
- patchrail/ci/__init__.py +7 -0
- patchrail/ci/classify.py +888 -0
- patchrail/cli.py +8566 -0
- patchrail/funded_issues/__init__.py +138 -0
- patchrail/funded_issues/algora_board.py +240 -0
- patchrail/funded_issues/blocklist.py +112 -0
- patchrail/funded_issues/discovery.py +4091 -0
- patchrail/funded_issues/importers.py +316 -0
- patchrail/funded_issues/source_noise.py +349 -0
- patchrail/funded_issues/store.py +459 -0
- patchrail/queue/__init__.py +75 -0
- patchrail/queue/server.py +273 -0
- patchrail/queue/status.py +756 -0
- patchrail/queue/store.py +600 -0
- patchrail/reviewer_quick_check.py +650 -0
- patchrail/schemas/__init__.py +1 -0
- patchrail/schemas/application-dossier.v1.schema.json +305 -0
- patchrail/schemas/ci-benchmark.v1.schema.json +174 -0
- patchrail/schemas/ci-fixture-check.v1.schema.json +122 -0
- patchrail/schemas/ci-pilot-metrics.v1.schema.json +164 -0
- patchrail/schemas/ci-pilot-summary.v1.schema.json +146 -0
- patchrail/schemas/ci-result.v1.schema.json +133 -0
- patchrail/schemas/funded-issues-client-report.v1.schema.json +524 -0
- patchrail/schemas/funded-issues-recheck-queue.v1.schema.json +333 -0
- patchrail/schemas/funded-issues-recheck-summary.v1.schema.json +136 -0
- patchrail/schemas/funded-issues-report.v1.schema.json +836 -0
- patchrail/schemas/funded-issues-shortlist.v1.schema.json +953 -0
- patchrail/schemas/funded-issues-store-status.v1.schema.json +96 -0
- patchrail/schemas/funded-issues-store.v1.schema.json +117 -0
- patchrail/schemas/queue-audit-event.v1.schema.json +44 -0
- patchrail/schemas/queue-audit-summary.v1.schema.json +169 -0
- patchrail/schemas/queue-gate-report.v1.schema.json +158 -0
- patchrail/schemas/queue-policy-resolution.v1.schema.json +188 -0
- patchrail/schemas/queue-policy-scan.v1.schema.json +175 -0
- patchrail/schemas/queue-proposal.v1.schema.json +61 -0
- patchrail/schemas/queue-review.v1.schema.json +218 -0
- patchrail/schemas/queue-status.v1.schema.json +179 -0
- patchrail/schemas/queue-work-item.v1.schema.json +64 -0
- patchrail/schemas/reviewer-quick-check-artifacts.v1.schema.json +104 -0
- patchrail/web_metrics.py +649 -0
- patchrail-0.1.0.dist-info/METADATA +279 -0
- patchrail-0.1.0.dist-info/RECORD +47 -0
- patchrail-0.1.0.dist-info/WHEEL +4 -0
- patchrail-0.1.0.dist-info/entry_points.txt +2 -0
- patchrail-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
REVIEWER_PACKET_SCHEMA_VERSION = "patchrail.reviewer_quick_check_artifacts.v1"
|
|
12
|
+
REVIEWER_PACKET_GENERATED_FROM = "local_checkout"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _run_patchrail(
|
|
16
|
+
args: list[str], *, root: Path, allow_nonzero: bool = False
|
|
17
|
+
) -> subprocess.CompletedProcess[str]:
|
|
18
|
+
proc = subprocess.run(
|
|
19
|
+
[sys.executable, "-m", "patchrail", *args],
|
|
20
|
+
cwd=root,
|
|
21
|
+
text=True,
|
|
22
|
+
capture_output=True,
|
|
23
|
+
check=False,
|
|
24
|
+
)
|
|
25
|
+
if proc.returncode != 0 and not allow_nonzero:
|
|
26
|
+
sys.stderr.write(proc.stderr)
|
|
27
|
+
raise SystemExit(proc.returncode)
|
|
28
|
+
return proc
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fenced(label: str, text: str) -> str:
|
|
32
|
+
cleaned = text.strip()
|
|
33
|
+
return f"```{label}\n{cleaned}\n```"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _display_path(path: Path, *, root: Path) -> str:
|
|
37
|
+
if not path.is_absolute():
|
|
38
|
+
return path.as_posix()
|
|
39
|
+
try:
|
|
40
|
+
return path.relative_to(root).as_posix()
|
|
41
|
+
except ValueError:
|
|
42
|
+
return path.name
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _release_readiness_markdown(payload: dict[str, object]) -> str:
|
|
46
|
+
checks = payload["checks"]
|
|
47
|
+
safety = payload["safety"]
|
|
48
|
+
if not isinstance(checks, dict) or not isinstance(safety, dict):
|
|
49
|
+
raise ValueError("release readiness payload must include checks and safety objects")
|
|
50
|
+
|
|
51
|
+
lines = [
|
|
52
|
+
"# PatchRail Release Readiness",
|
|
53
|
+
"",
|
|
54
|
+
f"- Schema: `{payload['schema_version']}`",
|
|
55
|
+
f"- Version: `{payload['version']}`",
|
|
56
|
+
f"- Published to PyPI: `{payload['published']}`",
|
|
57
|
+
f"- Build: `{checks['build']}`",
|
|
58
|
+
f"- Twine check: `{checks['twine_check']}`",
|
|
59
|
+
f"- Wheel smoke: `{checks['wheel_smoke']}`",
|
|
60
|
+
f"- Doctor status: `{checks['doctor_status']}`",
|
|
61
|
+
f"- Fixture smoke class: `{checks['fixture_failure_class']}`",
|
|
62
|
+
"",
|
|
63
|
+
"## Artifacts",
|
|
64
|
+
"",
|
|
65
|
+
]
|
|
66
|
+
artifacts = payload["artifacts"]
|
|
67
|
+
if not isinstance(artifacts, list):
|
|
68
|
+
raise ValueError("release readiness payload must include an artifacts list")
|
|
69
|
+
for artifact in artifacts:
|
|
70
|
+
if not isinstance(artifact, dict):
|
|
71
|
+
raise ValueError("release readiness artifacts must be objects")
|
|
72
|
+
lines.append(
|
|
73
|
+
f"- `{artifact['file']}`: sha256 `{artifact['sha256']}`, {artifact['size_bytes']} bytes"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
lines.extend(
|
|
77
|
+
[
|
|
78
|
+
"",
|
|
79
|
+
"## Safety",
|
|
80
|
+
"",
|
|
81
|
+
f"- Local-first: `{safety['local_first']}`",
|
|
82
|
+
f"- Created release tag: `{safety['created_release_tag']}`",
|
|
83
|
+
f"- Announced publicly: `{safety['announced_publicly']}`",
|
|
84
|
+
f"- Contacted third parties: `{safety['contacted_third_parties']}`",
|
|
85
|
+
f"- GitHub write permission required: `{safety['github_write_permission_required']}`",
|
|
86
|
+
f"- External model required: `{safety['external_model_required']}`",
|
|
87
|
+
"",
|
|
88
|
+
"## Manual Gates Remaining",
|
|
89
|
+
"",
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
gates = payload["manual_gates_remaining"]
|
|
93
|
+
if not isinstance(gates, list):
|
|
94
|
+
raise ValueError("release readiness payload must include manual gates")
|
|
95
|
+
lines.extend(f"- {gate}" for gate in gates)
|
|
96
|
+
return "\n".join(lines) + "\n"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _write_artifacts(
|
|
100
|
+
out_dir: Path,
|
|
101
|
+
*,
|
|
102
|
+
ci_report: str,
|
|
103
|
+
release_readiness_text: str,
|
|
104
|
+
release_readiness_json: str,
|
|
105
|
+
control_plane_text: str,
|
|
106
|
+
control_plane_json: str,
|
|
107
|
+
http_api_text: str,
|
|
108
|
+
http_api_json: str,
|
|
109
|
+
gate_text: str,
|
|
110
|
+
dossier_text: str,
|
|
111
|
+
dossier_json: str,
|
|
112
|
+
dossier_schema: str,
|
|
113
|
+
reviewer_packet_schema: str,
|
|
114
|
+
) -> list[str]:
|
|
115
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
artifacts = {
|
|
117
|
+
"README.md": _reviewer_packet_readme(),
|
|
118
|
+
"ci-triage-demo.md": ci_report,
|
|
119
|
+
"release-readiness.md": release_readiness_text,
|
|
120
|
+
"release-readiness.json": release_readiness_json,
|
|
121
|
+
"control-plane-evidence.md": control_plane_text,
|
|
122
|
+
"control-plane-evidence.json": control_plane_json,
|
|
123
|
+
"http-api-evidence.md": http_api_text,
|
|
124
|
+
"http-api-evidence.json": http_api_json,
|
|
125
|
+
"application-gate.txt": gate_text,
|
|
126
|
+
"application-dossier.txt": dossier_text,
|
|
127
|
+
"application-dossier.json": dossier_json,
|
|
128
|
+
"application-dossier.schema.json": dossier_schema,
|
|
129
|
+
"reviewer-quick-check-artifacts.schema.json": reviewer_packet_schema,
|
|
130
|
+
}
|
|
131
|
+
for name, content in artifacts.items():
|
|
132
|
+
(out_dir / name).write_text(content.strip() + "\n", encoding="utf-8")
|
|
133
|
+
return sorted(artifacts)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _artifact_detail(path: Path) -> dict[str, object]:
|
|
137
|
+
data = path.read_bytes()
|
|
138
|
+
return {
|
|
139
|
+
"path": path.name,
|
|
140
|
+
"size_bytes": len(data),
|
|
141
|
+
"sha256": hashlib.sha256(data).hexdigest(),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _artifact_purpose(name: str) -> str:
|
|
146
|
+
purposes = {
|
|
147
|
+
"README.md": "review order, integrity command, and safety boundary",
|
|
148
|
+
"reviewer-quick-check.md": "single-file walkthrough of local reviewer evidence",
|
|
149
|
+
"ci-triage-demo.md": "real CI Janitor output for the bundled fixture",
|
|
150
|
+
"release-readiness.md": "human-readable local build and publish-gate evidence",
|
|
151
|
+
"release-readiness.json": "structured local build and publish-gate evidence",
|
|
152
|
+
"control-plane-evidence.md": "human-readable Agent Control Plane handoff evidence",
|
|
153
|
+
"control-plane-evidence.json": "structured Agent Control Plane handoff evidence",
|
|
154
|
+
"http-api-evidence.md": "human-readable loopback HTTP API smoke evidence",
|
|
155
|
+
"http-api-evidence.json": "structured loopback HTTP API smoke evidence",
|
|
156
|
+
"application-gate.txt": "fail-closed application readiness gate output",
|
|
157
|
+
"application-dossier.txt": "human-readable draft-only application dossier",
|
|
158
|
+
"application-dossier.json": "structured draft-only application dossier",
|
|
159
|
+
"application-dossier.schema.json": "schema for the draft-only application dossier",
|
|
160
|
+
"reviewer-quick-check-artifacts.schema.json": "schema for this reviewer packet manifest",
|
|
161
|
+
}
|
|
162
|
+
return purposes.get(name, "reviewer packet artifact")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _write_artifact_index(out_dir: Path, artifact_names: list[str]) -> None:
|
|
166
|
+
details = [_artifact_detail(out_dir / name) for name in artifact_names]
|
|
167
|
+
lines = [
|
|
168
|
+
"# PatchRail Reviewer Packet Artifact Index",
|
|
169
|
+
"",
|
|
170
|
+
"This index lists the reviewer-facing packet artifacts in the suggested",
|
|
171
|
+
"inspection order. It is generated locally and does not perform network,",
|
|
172
|
+
"write, publish, pull request, comment, funded-issue, or application-submit",
|
|
173
|
+
"actions.",
|
|
174
|
+
"",
|
|
175
|
+
"## Artifacts",
|
|
176
|
+
"",
|
|
177
|
+
"| Artifact | Purpose | Size | SHA-256 |",
|
|
178
|
+
"| --- | --- | ---: | --- |",
|
|
179
|
+
]
|
|
180
|
+
for detail in details:
|
|
181
|
+
path = str(detail["path"])
|
|
182
|
+
lines.append(
|
|
183
|
+
f"| `{path}` | {_artifact_purpose(path)} | {detail['size_bytes']} | "
|
|
184
|
+
f"`{detail['sha256']}` |"
|
|
185
|
+
)
|
|
186
|
+
lines.extend(
|
|
187
|
+
[
|
|
188
|
+
"",
|
|
189
|
+
"## Safety Boundary",
|
|
190
|
+
"",
|
|
191
|
+
"- Network required: `False`",
|
|
192
|
+
"- Write action required: `False`",
|
|
193
|
+
"- Application form submission performed: `False`",
|
|
194
|
+
"- PyPI publish performed: `False`",
|
|
195
|
+
"- Third-party pull request, issue comment, or funded-issue claim performed: `False`",
|
|
196
|
+
"",
|
|
197
|
+
"`manifest.json` includes this index's own byte size and SHA-256 digest for",
|
|
198
|
+
"offline integrity verification.",
|
|
199
|
+
]
|
|
200
|
+
)
|
|
201
|
+
(out_dir / "artifact-index.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _write_manifest(out_dir: Path, artifact_names: list[str]) -> None:
|
|
205
|
+
manifest = {
|
|
206
|
+
"schema_version": REVIEWER_PACKET_SCHEMA_VERSION,
|
|
207
|
+
"generated_from": REVIEWER_PACKET_GENERATED_FROM,
|
|
208
|
+
"network_required": False,
|
|
209
|
+
"write_action_required": False,
|
|
210
|
+
"application_form_submission_performed": False,
|
|
211
|
+
"artifacts": artifact_names,
|
|
212
|
+
"artifact_details": [_artifact_detail(out_dir / name) for name in artifact_names],
|
|
213
|
+
}
|
|
214
|
+
(out_dir / "manifest.json").write_text(
|
|
215
|
+
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
|
216
|
+
encoding="utf-8",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _packet_display_path(packet_dir: Path) -> str:
|
|
221
|
+
if packet_dir.is_absolute():
|
|
222
|
+
return packet_dir.name
|
|
223
|
+
return packet_dir.as_posix()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _failed_integrity_payload(
|
|
227
|
+
packet_dir: Path,
|
|
228
|
+
*,
|
|
229
|
+
errors: list[str],
|
|
230
|
+
manifest_readable: bool,
|
|
231
|
+
) -> dict[str, object]:
|
|
232
|
+
return {
|
|
233
|
+
"schema_version": "patchrail.reviewer_packet_integrity.v1",
|
|
234
|
+
"packet_dir": _packet_display_path(packet_dir),
|
|
235
|
+
"status": "failed",
|
|
236
|
+
"errors": errors,
|
|
237
|
+
"counts": {"artifact_count": 0, "detail_count": 0, "verified_artifact_count": 0},
|
|
238
|
+
"checks": {
|
|
239
|
+
"manifest_readable": manifest_readable,
|
|
240
|
+
"schema_version_expected": False,
|
|
241
|
+
"generated_from_expected": False,
|
|
242
|
+
"artifacts_match_details": False,
|
|
243
|
+
"regular_artifact_files": False,
|
|
244
|
+
"all_hashes_match": False,
|
|
245
|
+
"all_sizes_match": False,
|
|
246
|
+
"no_extra_files": False,
|
|
247
|
+
"safety_flags_pass": False,
|
|
248
|
+
},
|
|
249
|
+
"missing_artifacts": [],
|
|
250
|
+
"extra_files": [],
|
|
251
|
+
"mismatches": [],
|
|
252
|
+
"verified_artifacts": [],
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def verify_reviewer_packet(packet_dir: Path) -> dict[str, object]:
|
|
257
|
+
manifest_path = packet_dir / "manifest.json"
|
|
258
|
+
errors: list[str] = []
|
|
259
|
+
mismatches: list[dict[str, object]] = []
|
|
260
|
+
verified_artifacts: list[dict[str, object]] = []
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
264
|
+
except FileNotFoundError:
|
|
265
|
+
return _failed_integrity_payload(
|
|
266
|
+
packet_dir, errors=["missing manifest.json"], manifest_readable=False
|
|
267
|
+
)
|
|
268
|
+
except json.JSONDecodeError as exc:
|
|
269
|
+
return _failed_integrity_payload(
|
|
270
|
+
packet_dir, errors=[f"invalid manifest.json: {exc.msg}"], manifest_readable=False
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if not isinstance(manifest, dict):
|
|
274
|
+
errors.append("manifest must be a JSON object")
|
|
275
|
+
manifest = {}
|
|
276
|
+
|
|
277
|
+
schema_version_expected = manifest.get("schema_version") == REVIEWER_PACKET_SCHEMA_VERSION
|
|
278
|
+
generated_from_expected = manifest.get("generated_from") == REVIEWER_PACKET_GENERATED_FROM
|
|
279
|
+
if not schema_version_expected:
|
|
280
|
+
errors.append(
|
|
281
|
+
"manifest schema_version must be "
|
|
282
|
+
f"{REVIEWER_PACKET_SCHEMA_VERSION}: {manifest.get('schema_version')}"
|
|
283
|
+
)
|
|
284
|
+
if not generated_from_expected:
|
|
285
|
+
errors.append(
|
|
286
|
+
"manifest generated_from must be "
|
|
287
|
+
f"{REVIEWER_PACKET_GENERATED_FROM}: {manifest.get('generated_from')}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
artifacts = manifest.get("artifacts")
|
|
291
|
+
artifact_details = manifest.get("artifact_details")
|
|
292
|
+
if not isinstance(artifacts, list) or not all(isinstance(item, str) for item in artifacts):
|
|
293
|
+
errors.append("manifest artifacts must be a list of file names")
|
|
294
|
+
artifacts = []
|
|
295
|
+
if not isinstance(artifact_details, list) or not all(
|
|
296
|
+
isinstance(item, dict) for item in artifact_details
|
|
297
|
+
):
|
|
298
|
+
errors.append("manifest artifact_details must be a list of objects")
|
|
299
|
+
artifact_details = []
|
|
300
|
+
|
|
301
|
+
details_by_path: dict[str, dict[str, object]] = {}
|
|
302
|
+
duplicate_details: list[str] = []
|
|
303
|
+
for detail in artifact_details:
|
|
304
|
+
path = detail.get("path")
|
|
305
|
+
if not isinstance(path, str):
|
|
306
|
+
errors.append("artifact detail path must be a string")
|
|
307
|
+
continue
|
|
308
|
+
if Path(path).is_absolute() or "/" in path or "\\" in path:
|
|
309
|
+
errors.append(f"artifact detail path must be a local file name: {path}")
|
|
310
|
+
continue
|
|
311
|
+
if path in details_by_path:
|
|
312
|
+
duplicate_details.append(path)
|
|
313
|
+
continue
|
|
314
|
+
details_by_path[path] = detail
|
|
315
|
+
|
|
316
|
+
for path in duplicate_details:
|
|
317
|
+
errors.append(f"duplicate artifact detail: {path}")
|
|
318
|
+
|
|
319
|
+
manifest_artifacts = list(artifacts)
|
|
320
|
+
detail_paths = list(details_by_path)
|
|
321
|
+
missing_details = sorted(set(manifest_artifacts) - set(detail_paths))
|
|
322
|
+
extra_details = sorted(set(detail_paths) - set(manifest_artifacts))
|
|
323
|
+
for path in missing_details:
|
|
324
|
+
errors.append(f"artifact missing detail: {path}")
|
|
325
|
+
for path in extra_details:
|
|
326
|
+
errors.append(f"artifact detail not listed in artifacts: {path}")
|
|
327
|
+
|
|
328
|
+
missing_artifacts: list[str] = []
|
|
329
|
+
hash_mismatch = False
|
|
330
|
+
size_mismatch = False
|
|
331
|
+
regular_artifact_files = True
|
|
332
|
+
for artifact in manifest_artifacts:
|
|
333
|
+
if Path(artifact).is_absolute() or "/" in artifact or "\\" in artifact:
|
|
334
|
+
errors.append(f"artifact path must be a local file name: {artifact}")
|
|
335
|
+
continue
|
|
336
|
+
artifact_path = packet_dir / artifact
|
|
337
|
+
detail = details_by_path.get(artifact)
|
|
338
|
+
if not artifact_path.exists():
|
|
339
|
+
missing_artifacts.append(artifact)
|
|
340
|
+
continue
|
|
341
|
+
if artifact_path.is_symlink():
|
|
342
|
+
regular_artifact_files = False
|
|
343
|
+
errors.append(f"artifact must not be a symlink: {artifact}")
|
|
344
|
+
continue
|
|
345
|
+
if not artifact_path.is_file():
|
|
346
|
+
regular_artifact_files = False
|
|
347
|
+
errors.append(f"artifact must be a regular file: {artifact}")
|
|
348
|
+
continue
|
|
349
|
+
actual = _artifact_detail(artifact_path)
|
|
350
|
+
if detail is None:
|
|
351
|
+
continue
|
|
352
|
+
expected_size = detail.get("size_bytes")
|
|
353
|
+
expected_sha = detail.get("sha256")
|
|
354
|
+
actual_size = actual["size_bytes"]
|
|
355
|
+
actual_sha = actual["sha256"]
|
|
356
|
+
item_mismatches: dict[str, object] = {"path": artifact}
|
|
357
|
+
if expected_size != actual_size:
|
|
358
|
+
size_mismatch = True
|
|
359
|
+
item_mismatches["expected_size_bytes"] = expected_size
|
|
360
|
+
item_mismatches["actual_size_bytes"] = actual_size
|
|
361
|
+
if expected_sha != actual_sha:
|
|
362
|
+
hash_mismatch = True
|
|
363
|
+
item_mismatches["expected_sha256"] = expected_sha
|
|
364
|
+
item_mismatches["actual_sha256"] = actual_sha
|
|
365
|
+
if len(item_mismatches) > 1:
|
|
366
|
+
mismatches.append(item_mismatches)
|
|
367
|
+
else:
|
|
368
|
+
verified_artifacts.append(actual)
|
|
369
|
+
|
|
370
|
+
extra_files = sorted(
|
|
371
|
+
path.name
|
|
372
|
+
for path in packet_dir.iterdir()
|
|
373
|
+
if path.name != "manifest.json" and path.name not in manifest_artifacts
|
|
374
|
+
)
|
|
375
|
+
safety_flags_pass = (
|
|
376
|
+
manifest.get("network_required") is False
|
|
377
|
+
and manifest.get("write_action_required") is False
|
|
378
|
+
and manifest.get("application_form_submission_performed") is False
|
|
379
|
+
)
|
|
380
|
+
if not safety_flags_pass:
|
|
381
|
+
errors.append("manifest safety flags must all be false")
|
|
382
|
+
|
|
383
|
+
checks = {
|
|
384
|
+
"manifest_readable": True,
|
|
385
|
+
"schema_version_expected": schema_version_expected,
|
|
386
|
+
"generated_from_expected": generated_from_expected,
|
|
387
|
+
"artifacts_match_details": not missing_details and not extra_details,
|
|
388
|
+
"regular_artifact_files": regular_artifact_files and not missing_artifacts,
|
|
389
|
+
"all_hashes_match": not hash_mismatch and not missing_artifacts,
|
|
390
|
+
"all_sizes_match": not size_mismatch and not missing_artifacts,
|
|
391
|
+
"no_extra_files": not extra_files,
|
|
392
|
+
"safety_flags_pass": safety_flags_pass,
|
|
393
|
+
}
|
|
394
|
+
status = (
|
|
395
|
+
"verified"
|
|
396
|
+
if not errors
|
|
397
|
+
and not missing_artifacts
|
|
398
|
+
and not extra_files
|
|
399
|
+
and not mismatches
|
|
400
|
+
and all(checks.values())
|
|
401
|
+
else "failed"
|
|
402
|
+
)
|
|
403
|
+
return {
|
|
404
|
+
"schema_version": "patchrail.reviewer_packet_integrity.v1",
|
|
405
|
+
"packet_dir": _packet_display_path(packet_dir),
|
|
406
|
+
"status": status,
|
|
407
|
+
"manifest_schema_version": manifest.get("schema_version"),
|
|
408
|
+
"counts": {
|
|
409
|
+
"artifact_count": len(manifest_artifacts),
|
|
410
|
+
"detail_count": len(details_by_path),
|
|
411
|
+
"verified_artifact_count": len(verified_artifacts),
|
|
412
|
+
},
|
|
413
|
+
"checks": checks,
|
|
414
|
+
"errors": errors,
|
|
415
|
+
"missing_artifacts": missing_artifacts,
|
|
416
|
+
"extra_files": extra_files,
|
|
417
|
+
"mismatches": mismatches,
|
|
418
|
+
"verified_artifacts": verified_artifacts,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _reviewer_packet_readme() -> str:
|
|
423
|
+
return """
|
|
424
|
+
# PatchRail Reviewer Packet
|
|
425
|
+
|
|
426
|
+
This directory is a local, reviewer-facing evidence bundle generated from a
|
|
427
|
+
checked-out PatchRail source tree.
|
|
428
|
+
|
|
429
|
+
## Review Order
|
|
430
|
+
|
|
431
|
+
1. `reviewer-quick-check.md` - human-readable walkthrough of the local smoke
|
|
432
|
+
test, CI triage demo, Agent Control Plane evidence, fail-closed application
|
|
433
|
+
gate, and application dossier contract.
|
|
434
|
+
2. `ci-triage-demo.md` - real local CI Janitor output for the bundled fixture.
|
|
435
|
+
3. `release-readiness.md` and `release-readiness.json` - local build,
|
|
436
|
+
`twine check`, wheel smoke, and manual publish gates. They do not publish to
|
|
437
|
+
PyPI or create a release tag.
|
|
438
|
+
4. `control-plane-evidence.md` and `control-plane-evidence.json` - local queue
|
|
439
|
+
handoff evidence with human gates complete and execution disabled.
|
|
440
|
+
5. `http-api-evidence.md` and `http-api-evidence.json` - ephemeral
|
|
441
|
+
`127.0.0.1` HTTP API smoke evidence with endpoints and human gates checked.
|
|
442
|
+
6. `application-gate.txt` - expected fail-closed result until public evidence
|
|
443
|
+
is real.
|
|
444
|
+
7. `application-dossier.txt` and `application-dossier.json` - local draft
|
|
445
|
+
dossier; it does not submit any external form.
|
|
446
|
+
8. `manifest.json` plus the schema files - offline validation contract.
|
|
447
|
+
|
|
448
|
+
## Integrity Check
|
|
449
|
+
|
|
450
|
+
After copying or downloading this directory, verify it before review:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
patchrail evidence verify-reviewer-packet patchrail-reviewer-packet --format markdown
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The verifier recomputes every listed artifact's byte size and SHA-256 digest,
|
|
457
|
+
rejects symlinked or non-file artifacts, rejects extra files, and exits
|
|
458
|
+
non-zero if the packet has been tampered with or drifted from its manifest.
|
|
459
|
+
|
|
460
|
+
## Safety Boundary
|
|
461
|
+
|
|
462
|
+
- Network required: `False`
|
|
463
|
+
- Write action required: `False`
|
|
464
|
+
- Application form submission performed: `False`
|
|
465
|
+
- PyPI publish performed: `False`
|
|
466
|
+
- Third-party pull request, issue comment, or funded-issue claim performed:
|
|
467
|
+
`False`
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def build_reviewer_quick_check(*, root: Path, out_dir: Path | None = None) -> str:
|
|
472
|
+
doctor = _run_patchrail(["doctor", "--format", "text"], root=root)
|
|
473
|
+
ci_report = _run_patchrail(
|
|
474
|
+
[
|
|
475
|
+
"ci",
|
|
476
|
+
"explain",
|
|
477
|
+
"--log",
|
|
478
|
+
"examples/ci-triage/dependency-failure.log",
|
|
479
|
+
"--format",
|
|
480
|
+
"markdown",
|
|
481
|
+
],
|
|
482
|
+
root=root,
|
|
483
|
+
)
|
|
484
|
+
release_readiness_json = _run_patchrail(
|
|
485
|
+
["evidence", "release-readiness", "--clean-dist", "--format", "json"],
|
|
486
|
+
root=root,
|
|
487
|
+
)
|
|
488
|
+
release_readiness_text = _release_readiness_markdown(json.loads(release_readiness_json.stdout))
|
|
489
|
+
control_plane = _run_patchrail(
|
|
490
|
+
["evidence", "control-plane", "--format", "markdown"],
|
|
491
|
+
root=root,
|
|
492
|
+
)
|
|
493
|
+
control_plane_json = _run_patchrail(
|
|
494
|
+
["evidence", "control-plane", "--format", "json"],
|
|
495
|
+
root=root,
|
|
496
|
+
)
|
|
497
|
+
http_api = _run_patchrail(
|
|
498
|
+
["evidence", "http-api", "--format", "markdown"],
|
|
499
|
+
root=root,
|
|
500
|
+
)
|
|
501
|
+
http_api_json = _run_patchrail(
|
|
502
|
+
["evidence", "http-api", "--format", "json"],
|
|
503
|
+
root=root,
|
|
504
|
+
)
|
|
505
|
+
gate = _run_patchrail(
|
|
506
|
+
["evidence", "application-gate", "--format", "text"],
|
|
507
|
+
root=root,
|
|
508
|
+
allow_nonzero=True,
|
|
509
|
+
)
|
|
510
|
+
dossier = _run_patchrail(["evidence", "application-dossier", "--format", "text"], root=root)
|
|
511
|
+
dossier_json = _run_patchrail(
|
|
512
|
+
["evidence", "application-dossier", "--format", "json"], root=root
|
|
513
|
+
)
|
|
514
|
+
dossier_schema = _run_patchrail(["schema", "application-dossier"], root=root)
|
|
515
|
+
reviewer_packet_schema = _run_patchrail(["schema", "reviewer-quick-check-artifacts"], root=root)
|
|
516
|
+
|
|
517
|
+
lines = [
|
|
518
|
+
"# PatchRail Reviewer Quick Check",
|
|
519
|
+
"",
|
|
520
|
+
"This local smoke test uses the checked-out source tree only. It does not",
|
|
521
|
+
"publish to PyPI, create pull requests, post comments, claim funding, call",
|
|
522
|
+
"external models, or require GitHub write permissions.",
|
|
523
|
+
"",
|
|
524
|
+
"## 1. Local Doctor",
|
|
525
|
+
"",
|
|
526
|
+
_fenced("text", doctor.stdout),
|
|
527
|
+
"",
|
|
528
|
+
"## 2. CI Triage Demo",
|
|
529
|
+
"",
|
|
530
|
+
_fenced(
|
|
531
|
+
"bash",
|
|
532
|
+
(
|
|
533
|
+
"uv run --extra dev patchrail ci explain --log "
|
|
534
|
+
"examples/ci-triage/dependency-failure.log --format markdown"
|
|
535
|
+
),
|
|
536
|
+
),
|
|
537
|
+
"",
|
|
538
|
+
_fenced("markdown", ci_report.stdout),
|
|
539
|
+
"",
|
|
540
|
+
"## 3. Release Readiness Evidence",
|
|
541
|
+
"",
|
|
542
|
+
"This local evidence builds and smoke-tests release artifacts, but leaves",
|
|
543
|
+
"PyPI publish, release tagging, public announcements, and external program",
|
|
544
|
+
"submission behind manual gates.",
|
|
545
|
+
"",
|
|
546
|
+
_fenced("markdown", release_readiness_text),
|
|
547
|
+
"",
|
|
548
|
+
"## 4. Agent Control Plane Evidence",
|
|
549
|
+
"",
|
|
550
|
+
"The local queue demo must be ready for reviewer handoff, exercise human",
|
|
551
|
+
"approval gates, and keep execution disabled.",
|
|
552
|
+
"",
|
|
553
|
+
_fenced("markdown", control_plane.stdout),
|
|
554
|
+
"",
|
|
555
|
+
"## 5. HTTP API Evidence",
|
|
556
|
+
"",
|
|
557
|
+
"The local HTTP smoke starts an ephemeral loopback server, exercises the",
|
|
558
|
+
"public endpoints, records approval/rejection decisions, and confirms",
|
|
559
|
+
"that write actions remain locked.",
|
|
560
|
+
"",
|
|
561
|
+
_fenced("markdown", http_api.stdout),
|
|
562
|
+
"",
|
|
563
|
+
"## 6. Application Gate",
|
|
564
|
+
"",
|
|
565
|
+
"The gate is expected to fail closed until public evidence is real.",
|
|
566
|
+
"",
|
|
567
|
+
_fenced("text", gate.stdout),
|
|
568
|
+
"",
|
|
569
|
+
"## 7. Application Dossier Contract",
|
|
570
|
+
"",
|
|
571
|
+
"The dossier is a local draft artifact. It does not submit the external",
|
|
572
|
+
"application and keeps maintainer tap required.",
|
|
573
|
+
"",
|
|
574
|
+
_fenced("text", dossier.stdout),
|
|
575
|
+
"",
|
|
576
|
+
"Schema smoke:",
|
|
577
|
+
"",
|
|
578
|
+
_fenced("json", dossier_schema.stdout),
|
|
579
|
+
"",
|
|
580
|
+
"Reviewer packet manifest schema smoke:",
|
|
581
|
+
"",
|
|
582
|
+
_fenced("json", reviewer_packet_schema.stdout),
|
|
583
|
+
"",
|
|
584
|
+
]
|
|
585
|
+
written_artifacts: list[str] = []
|
|
586
|
+
if out_dir:
|
|
587
|
+
base_artifacts = _write_artifacts(
|
|
588
|
+
out_dir,
|
|
589
|
+
ci_report=ci_report.stdout,
|
|
590
|
+
release_readiness_text=release_readiness_text,
|
|
591
|
+
release_readiness_json=release_readiness_json.stdout,
|
|
592
|
+
control_plane_text=control_plane.stdout,
|
|
593
|
+
control_plane_json=control_plane_json.stdout,
|
|
594
|
+
http_api_text=http_api.stdout,
|
|
595
|
+
http_api_json=http_api_json.stdout,
|
|
596
|
+
gate_text=gate.stdout,
|
|
597
|
+
dossier_text=dossier.stdout,
|
|
598
|
+
dossier_json=dossier_json.stdout,
|
|
599
|
+
dossier_schema=dossier_schema.stdout,
|
|
600
|
+
reviewer_packet_schema=reviewer_packet_schema.stdout,
|
|
601
|
+
)
|
|
602
|
+
written_artifacts = ["reviewer-quick-check.md", "artifact-index.md", *base_artifacts]
|
|
603
|
+
lines.extend(
|
|
604
|
+
[
|
|
605
|
+
"## 8. Artifact Packet",
|
|
606
|
+
"",
|
|
607
|
+
f"Output directory: `{_display_path(out_dir, root=root)}`",
|
|
608
|
+
"",
|
|
609
|
+
*[f"- `{name}`" for name in [*written_artifacts, "manifest.json"]],
|
|
610
|
+
"",
|
|
611
|
+
]
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
lines.extend(
|
|
615
|
+
[
|
|
616
|
+
"## Result",
|
|
617
|
+
"",
|
|
618
|
+
"- Reviewer demo generated: `True`",
|
|
619
|
+
"- Release readiness evidence generated: `True`",
|
|
620
|
+
"- Agent Control Plane evidence generated: `True`",
|
|
621
|
+
"- HTTP API evidence generated: `True`",
|
|
622
|
+
"- Application dossier generated: `True`",
|
|
623
|
+
"- Application dossier schema available: `True`",
|
|
624
|
+
"- Reviewer packet manifest schema available: `True`",
|
|
625
|
+
f"- Artifact packet generated: `{'True' if written_artifacts else 'False'}`",
|
|
626
|
+
"- Network required: `False`",
|
|
627
|
+
"- Write action required: `False`",
|
|
628
|
+
"- Application form submission performed: `False`",
|
|
629
|
+
]
|
|
630
|
+
)
|
|
631
|
+
final_output = "\n".join(lines)
|
|
632
|
+
if out_dir:
|
|
633
|
+
(out_dir / "reviewer-quick-check.md").write_text(final_output + "\n", encoding="utf-8")
|
|
634
|
+
_write_artifact_index(out_dir, ["reviewer-quick-check.md", *base_artifacts])
|
|
635
|
+
_write_manifest(out_dir, written_artifacts)
|
|
636
|
+
return final_output + "\n"
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def main(argv: list[str] | None = None, *, root: Path | None = None) -> int:
|
|
640
|
+
parser = argparse.ArgumentParser(
|
|
641
|
+
description="Generate a local PatchRail reviewer quick check packet."
|
|
642
|
+
)
|
|
643
|
+
parser.add_argument(
|
|
644
|
+
"--out-dir",
|
|
645
|
+
type=Path,
|
|
646
|
+
help="Optional directory for reviewer-facing Markdown/JSON artifacts.",
|
|
647
|
+
)
|
|
648
|
+
args = parser.parse_args(argv)
|
|
649
|
+
print(build_reviewer_quick_check(root=root or Path("."), out_dir=args.out_dir), end="")
|
|
650
|
+
return 0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Versioned JSON schemas bundled with PatchRail."""
|