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.
Files changed (47) hide show
  1. patchrail/__init__.py +7 -0
  2. patchrail/__main__.py +7 -0
  3. patchrail/ci/__init__.py +7 -0
  4. patchrail/ci/classify.py +888 -0
  5. patchrail/cli.py +8566 -0
  6. patchrail/funded_issues/__init__.py +138 -0
  7. patchrail/funded_issues/algora_board.py +240 -0
  8. patchrail/funded_issues/blocklist.py +112 -0
  9. patchrail/funded_issues/discovery.py +4091 -0
  10. patchrail/funded_issues/importers.py +316 -0
  11. patchrail/funded_issues/source_noise.py +349 -0
  12. patchrail/funded_issues/store.py +459 -0
  13. patchrail/queue/__init__.py +75 -0
  14. patchrail/queue/server.py +273 -0
  15. patchrail/queue/status.py +756 -0
  16. patchrail/queue/store.py +600 -0
  17. patchrail/reviewer_quick_check.py +650 -0
  18. patchrail/schemas/__init__.py +1 -0
  19. patchrail/schemas/application-dossier.v1.schema.json +305 -0
  20. patchrail/schemas/ci-benchmark.v1.schema.json +174 -0
  21. patchrail/schemas/ci-fixture-check.v1.schema.json +122 -0
  22. patchrail/schemas/ci-pilot-metrics.v1.schema.json +164 -0
  23. patchrail/schemas/ci-pilot-summary.v1.schema.json +146 -0
  24. patchrail/schemas/ci-result.v1.schema.json +133 -0
  25. patchrail/schemas/funded-issues-client-report.v1.schema.json +524 -0
  26. patchrail/schemas/funded-issues-recheck-queue.v1.schema.json +333 -0
  27. patchrail/schemas/funded-issues-recheck-summary.v1.schema.json +136 -0
  28. patchrail/schemas/funded-issues-report.v1.schema.json +836 -0
  29. patchrail/schemas/funded-issues-shortlist.v1.schema.json +953 -0
  30. patchrail/schemas/funded-issues-store-status.v1.schema.json +96 -0
  31. patchrail/schemas/funded-issues-store.v1.schema.json +117 -0
  32. patchrail/schemas/queue-audit-event.v1.schema.json +44 -0
  33. patchrail/schemas/queue-audit-summary.v1.schema.json +169 -0
  34. patchrail/schemas/queue-gate-report.v1.schema.json +158 -0
  35. patchrail/schemas/queue-policy-resolution.v1.schema.json +188 -0
  36. patchrail/schemas/queue-policy-scan.v1.schema.json +175 -0
  37. patchrail/schemas/queue-proposal.v1.schema.json +61 -0
  38. patchrail/schemas/queue-review.v1.schema.json +218 -0
  39. patchrail/schemas/queue-status.v1.schema.json +179 -0
  40. patchrail/schemas/queue-work-item.v1.schema.json +64 -0
  41. patchrail/schemas/reviewer-quick-check-artifacts.v1.schema.json +104 -0
  42. patchrail/web_metrics.py +649 -0
  43. patchrail-0.1.0.dist-info/METADATA +279 -0
  44. patchrail-0.1.0.dist-info/RECORD +47 -0
  45. patchrail-0.1.0.dist-info/WHEEL +4 -0
  46. patchrail-0.1.0.dist-info/entry_points.txt +2 -0
  47. 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."""