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,756 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from patchrail import __version__
|
|
10
|
+
from patchrail.queue.store import (
|
|
11
|
+
DEFAULT_QUEUE_PATH,
|
|
12
|
+
export_audit_events,
|
|
13
|
+
init_queue,
|
|
14
|
+
list_proposals,
|
|
15
|
+
list_work_items,
|
|
16
|
+
reject_proposal,
|
|
17
|
+
skip_work_item,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
QUEUE_STATUS_SCHEMA_VERSION = "patchrail.queue_status.v1"
|
|
21
|
+
QUEUE_AUDIT_SUMMARY_SCHEMA_VERSION = "patchrail.queue_audit_summary.v1"
|
|
22
|
+
QUEUE_BUNDLE_SCHEMA_VERSION = "patchrail.queue_bundle.v1"
|
|
23
|
+
QUEUE_GATE_REPORT_SCHEMA_VERSION = "patchrail.queue_gate_report.v1"
|
|
24
|
+
QUEUE_REVIEW_SCHEMA_VERSION = "patchrail.queue_review.v1"
|
|
25
|
+
QUEUE_POLICY_SCAN_SCHEMA_VERSION = "patchrail.queue_policy_scan.v1"
|
|
26
|
+
QUEUE_POLICY_RESOLUTION_SCHEMA_VERSION = "patchrail.queue_policy_resolution.v1"
|
|
27
|
+
DEFAULT_POLICY_RESOLUTION_REASON = "revenue-safety policy: local-only human approval required"
|
|
28
|
+
|
|
29
|
+
DEFAULT_REQUIRED_AUDIT_EVENTS = [
|
|
30
|
+
"work_item_added",
|
|
31
|
+
"proposal_added",
|
|
32
|
+
"proposal_approved",
|
|
33
|
+
"proposal_rejected",
|
|
34
|
+
"work_item_approved",
|
|
35
|
+
"work_item_rejected",
|
|
36
|
+
"work_items_exported",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
SAFE_QUEUE_REQUIREMENTS = {
|
|
40
|
+
"billing_required": False,
|
|
41
|
+
"external_model_required": False,
|
|
42
|
+
"network_required": False,
|
|
43
|
+
"github_write_permission_required": False,
|
|
44
|
+
"write_actions_allowed_by_default": False,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
SAFE_QUEUE_STATUS = {
|
|
48
|
+
**SAFE_QUEUE_REQUIREMENTS,
|
|
49
|
+
"approval_records_execute_actions": False,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
LOCAL_PATH_PATTERN = re.compile(
|
|
53
|
+
r"(/Volumes|/Users|/home|/tmp|/private/tmp|/var/folders|/private/var)/[^\s\"'`]+"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
POLICY_SCAN_RULES = {
|
|
57
|
+
"automatic_pull_request": [
|
|
58
|
+
"automatic pr",
|
|
59
|
+
"automatic pull request",
|
|
60
|
+
"auto pr",
|
|
61
|
+
"auto-submit",
|
|
62
|
+
"auto submit",
|
|
63
|
+
"open pr automatically",
|
|
64
|
+
"open pull request automatically",
|
|
65
|
+
],
|
|
66
|
+
"automatic_issue_comment": [
|
|
67
|
+
"automatic issue comment",
|
|
68
|
+
"comment automatically",
|
|
69
|
+
"mass comment",
|
|
70
|
+
"post a comment",
|
|
71
|
+
"post comment",
|
|
72
|
+
],
|
|
73
|
+
"outbound_contact": [
|
|
74
|
+
"contact maintainers",
|
|
75
|
+
"devrel",
|
|
76
|
+
"lead",
|
|
77
|
+
"outbound",
|
|
78
|
+
"sales",
|
|
79
|
+
"send email",
|
|
80
|
+
],
|
|
81
|
+
"funding_or_claim": [
|
|
82
|
+
"bounty",
|
|
83
|
+
"claim reward",
|
|
84
|
+
"funding claim",
|
|
85
|
+
"invoice",
|
|
86
|
+
"paid pilot",
|
|
87
|
+
"payment link",
|
|
88
|
+
"payout",
|
|
89
|
+
"pricing",
|
|
90
|
+
],
|
|
91
|
+
"identity_or_money_gate": [
|
|
92
|
+
"bank",
|
|
93
|
+
"card",
|
|
94
|
+
"government id",
|
|
95
|
+
"kyc",
|
|
96
|
+
"phone",
|
|
97
|
+
"postal address",
|
|
98
|
+
"tax form",
|
|
99
|
+
],
|
|
100
|
+
"external_write": [
|
|
101
|
+
"create release",
|
|
102
|
+
"create tag",
|
|
103
|
+
"external repo",
|
|
104
|
+
"force-push",
|
|
105
|
+
"gh issue comment",
|
|
106
|
+
"gh pr create",
|
|
107
|
+
"gh release create",
|
|
108
|
+
"git push",
|
|
109
|
+
"merge pull request",
|
|
110
|
+
"open issue comment",
|
|
111
|
+
"publish release",
|
|
112
|
+
"push to main",
|
|
113
|
+
"tag release",
|
|
114
|
+
"third-party",
|
|
115
|
+
],
|
|
116
|
+
"package_publish": [
|
|
117
|
+
"cargo publish",
|
|
118
|
+
"npm publish",
|
|
119
|
+
"publish package",
|
|
120
|
+
"publish to npm",
|
|
121
|
+
"publish to pypi",
|
|
122
|
+
"twine upload",
|
|
123
|
+
],
|
|
124
|
+
"external_application_submission": [
|
|
125
|
+
"application form",
|
|
126
|
+
"send application",
|
|
127
|
+
"submit application",
|
|
128
|
+
"submit external form",
|
|
129
|
+
"submit form",
|
|
130
|
+
],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _redact_local_paths(value: Any) -> Any:
|
|
135
|
+
if isinstance(value, dict):
|
|
136
|
+
return {key: _redact_local_paths(item) for key, item in value.items()}
|
|
137
|
+
if isinstance(value, list):
|
|
138
|
+
return [_redact_local_paths(item) for item in value]
|
|
139
|
+
if isinstance(value, str):
|
|
140
|
+
return LOCAL_PATH_PATTERN.sub(
|
|
141
|
+
lambda match: f"<local-path>/{Path(match.group(0)).name}",
|
|
142
|
+
value,
|
|
143
|
+
)
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _policy_scan_text(value: Any) -> str:
|
|
148
|
+
if value is None:
|
|
149
|
+
return ""
|
|
150
|
+
if isinstance(value, str):
|
|
151
|
+
return value
|
|
152
|
+
return json.dumps(value, sort_keys=True, ensure_ascii=True)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _policy_matches(text: str) -> list[dict[str, str]]:
|
|
156
|
+
normalized = " ".join(text.lower().replace("_", " ").replace("-", " ").split())
|
|
157
|
+
matches: list[dict[str, str]] = []
|
|
158
|
+
for category, terms in POLICY_SCAN_RULES.items():
|
|
159
|
+
for term in terms:
|
|
160
|
+
normalized_term = " ".join(term.lower().replace("_", " ").replace("-", " ").split())
|
|
161
|
+
if normalized_term in normalized:
|
|
162
|
+
matches.append({"category": category, "term": term})
|
|
163
|
+
return matches
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _policy_scan_work_item(item: dict[str, Any]) -> dict[str, Any] | None:
|
|
167
|
+
if item["status"] == "skipped" or item["approval_state"] == "rejected":
|
|
168
|
+
return None
|
|
169
|
+
haystack = "\n".join(
|
|
170
|
+
[
|
|
171
|
+
_policy_scan_text(item.get("kind")),
|
|
172
|
+
_policy_scan_text(item.get("title")),
|
|
173
|
+
_policy_scan_text(item.get("source")),
|
|
174
|
+
_policy_scan_text(item.get("payload")),
|
|
175
|
+
_policy_scan_text(item.get("decision_note")),
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
matches = _policy_matches(haystack)
|
|
179
|
+
if not matches:
|
|
180
|
+
return None
|
|
181
|
+
return {
|
|
182
|
+
"record_type": "work_item",
|
|
183
|
+
"id": item["id"],
|
|
184
|
+
"title": item["title"],
|
|
185
|
+
"status": item["status"],
|
|
186
|
+
"approval_state": item["approval_state"],
|
|
187
|
+
"source": item["source"],
|
|
188
|
+
"matched_categories": sorted({match["category"] for match in matches}),
|
|
189
|
+
"matched_terms": sorted({match["term"] for match in matches}),
|
|
190
|
+
"recommended_action": "reject_or_skip_before_handoff",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _policy_scan_proposal(proposal: dict[str, Any]) -> dict[str, Any] | None:
|
|
195
|
+
if proposal["approval_state"] == "rejected":
|
|
196
|
+
return None
|
|
197
|
+
haystack = "\n".join(
|
|
198
|
+
[
|
|
199
|
+
_policy_scan_text(proposal.get("title")),
|
|
200
|
+
_policy_scan_text(proposal.get("summary")),
|
|
201
|
+
_policy_scan_text(proposal.get("patch_plan")),
|
|
202
|
+
_policy_scan_text(proposal.get("decision_note")),
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
matches = _policy_matches(haystack)
|
|
206
|
+
if not matches:
|
|
207
|
+
return None
|
|
208
|
+
return {
|
|
209
|
+
"record_type": "proposal",
|
|
210
|
+
"id": proposal["id"],
|
|
211
|
+
"work_item_id": proposal["work_item_id"],
|
|
212
|
+
"title": proposal["title"],
|
|
213
|
+
"risk_level": proposal["risk_level"],
|
|
214
|
+
"approval_state": proposal["approval_state"],
|
|
215
|
+
"matched_categories": sorted({match["category"] for match in matches}),
|
|
216
|
+
"matched_terms": sorted({match["term"] for match in matches}),
|
|
217
|
+
"recommended_action": "reject_before_handoff",
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def queue_status_payload(
|
|
222
|
+
db_path: Path = DEFAULT_QUEUE_PATH,
|
|
223
|
+
*,
|
|
224
|
+
include_api_compat: bool = False,
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
init_result = init_queue(db_path)
|
|
227
|
+
work_items = _redact_local_paths([item.to_dict() for item in list_work_items(db_path=db_path)])
|
|
228
|
+
proposals = _redact_local_paths(
|
|
229
|
+
[proposal.to_dict() for proposal in list_proposals(db_path=db_path)]
|
|
230
|
+
)
|
|
231
|
+
audit_events = _redact_local_paths(export_audit_events(db_path=db_path)["audit_events"])
|
|
232
|
+
work_item_approval_counts = Counter(item["approval_state"] for item in work_items)
|
|
233
|
+
work_item_status_counts = Counter(item["status"] for item in work_items)
|
|
234
|
+
proposal_approval_counts = Counter(proposal["approval_state"] for proposal in proposals)
|
|
235
|
+
pending_work_items = work_item_approval_counts.get("pending", 0)
|
|
236
|
+
pending_proposals = proposal_approval_counts.get("pending", 0)
|
|
237
|
+
total_pending_decisions = pending_work_items + pending_proposals
|
|
238
|
+
latest_audit_event = audit_events[-1] if audit_events else None
|
|
239
|
+
payload: dict[str, Any] = {
|
|
240
|
+
"schema_version": QUEUE_STATUS_SCHEMA_VERSION,
|
|
241
|
+
"queue_schema_version": init_result["schema_version"],
|
|
242
|
+
"patchrail_version": __version__,
|
|
243
|
+
"db_path": _redact_local_paths(str(db_path)),
|
|
244
|
+
"local_first": True,
|
|
245
|
+
"host_boundary": "127.0.0.1 only by default",
|
|
246
|
+
"counts": {
|
|
247
|
+
"work_items_total": len(work_items),
|
|
248
|
+
"work_items_by_status": dict(sorted(work_item_status_counts.items())),
|
|
249
|
+
"work_items_by_approval_state": dict(sorted(work_item_approval_counts.items())),
|
|
250
|
+
"proposals_total": len(proposals),
|
|
251
|
+
"proposals_by_approval_state": dict(sorted(proposal_approval_counts.items())),
|
|
252
|
+
"audit_events_total": len(audit_events),
|
|
253
|
+
},
|
|
254
|
+
"human_gate_summary": {
|
|
255
|
+
"status": (
|
|
256
|
+
"awaiting_human_review" if total_pending_decisions > 0 else "no_pending_decisions"
|
|
257
|
+
),
|
|
258
|
+
"pending_work_items": pending_work_items,
|
|
259
|
+
"pending_proposals": pending_proposals,
|
|
260
|
+
"total_pending_decisions": total_pending_decisions,
|
|
261
|
+
"approved_work_items": work_item_approval_counts.get("approved", 0),
|
|
262
|
+
"rejected_work_items": work_item_approval_counts.get("rejected", 0),
|
|
263
|
+
"approved_proposals": proposal_approval_counts.get("approved", 0),
|
|
264
|
+
"rejected_proposals": proposal_approval_counts.get("rejected", 0),
|
|
265
|
+
"write_actions_unlocked": False,
|
|
266
|
+
},
|
|
267
|
+
"latest_audit_event": latest_audit_event,
|
|
268
|
+
"safety": SAFE_QUEUE_STATUS,
|
|
269
|
+
}
|
|
270
|
+
if include_api_compat:
|
|
271
|
+
payload["requirements"] = SAFE_QUEUE_REQUIREMENTS
|
|
272
|
+
payload["queue"] = {
|
|
273
|
+
"schema_version": init_result["schema_version"],
|
|
274
|
+
"work_items": len(work_items),
|
|
275
|
+
"pending_work_items": work_item_approval_counts.get("pending", 0),
|
|
276
|
+
"proposals": len(proposals),
|
|
277
|
+
"pending_proposals": proposal_approval_counts.get("pending", 0),
|
|
278
|
+
}
|
|
279
|
+
return payload
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def queue_audit_summary_payload(
|
|
283
|
+
db_path: Path = DEFAULT_QUEUE_PATH,
|
|
284
|
+
*,
|
|
285
|
+
required_events: list[str] | None = None,
|
|
286
|
+
) -> dict[str, Any]:
|
|
287
|
+
init_result = init_queue(db_path)
|
|
288
|
+
audit_events = export_audit_events(db_path=db_path)["audit_events"]
|
|
289
|
+
work_items = [item.to_dict() for item in list_work_items(db_path=db_path)]
|
|
290
|
+
proposals = [proposal.to_dict() for proposal in list_proposals(db_path=db_path)]
|
|
291
|
+
event_counts = Counter(event["event_type"] for event in audit_events)
|
|
292
|
+
required = required_events or DEFAULT_REQUIRED_AUDIT_EVENTS
|
|
293
|
+
missing_required_events = [event for event in required if event_counts.get(event, 0) == 0]
|
|
294
|
+
affected_work_items = sorted(
|
|
295
|
+
{
|
|
296
|
+
str(event["work_item_id"])
|
|
297
|
+
for event in audit_events
|
|
298
|
+
if event.get("work_item_id") is not None
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
gates = {
|
|
302
|
+
"work_item_approval_gate_exercised": event_counts.get("work_item_approved", 0) > 0,
|
|
303
|
+
"work_item_rejection_gate_exercised": event_counts.get("work_item_rejected", 0) > 0,
|
|
304
|
+
"work_item_skip_gate_exercised": event_counts.get("work_item_skipped", 0) > 0,
|
|
305
|
+
"proposal_approval_gate_exercised": event_counts.get("proposal_approved", 0) > 0,
|
|
306
|
+
"proposal_rejection_gate_exercised": event_counts.get("proposal_rejected", 0) > 0,
|
|
307
|
+
"queue_export_recorded": event_counts.get("work_items_exported", 0) > 0,
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
"schema_version": QUEUE_AUDIT_SUMMARY_SCHEMA_VERSION,
|
|
311
|
+
"queue_schema_version": init_result["schema_version"],
|
|
312
|
+
"patchrail_version": __version__,
|
|
313
|
+
"db_path": _redact_local_paths(str(db_path)),
|
|
314
|
+
"local_first": True,
|
|
315
|
+
"status": "human_gates_exercised"
|
|
316
|
+
if not missing_required_events
|
|
317
|
+
else "needs_more_audit_evidence",
|
|
318
|
+
"counts": {
|
|
319
|
+
"audit_events_total": len(audit_events),
|
|
320
|
+
"event_types": dict(sorted(event_counts.items())),
|
|
321
|
+
"work_items_total": len(work_items),
|
|
322
|
+
"proposals_total": len(proposals),
|
|
323
|
+
"affected_work_items": len(affected_work_items),
|
|
324
|
+
},
|
|
325
|
+
"required_events": required,
|
|
326
|
+
"missing_required_events": missing_required_events,
|
|
327
|
+
"gates": gates,
|
|
328
|
+
"affected_work_item_ids": affected_work_items,
|
|
329
|
+
"latest_audit_event": audit_events[-1] if audit_events else None,
|
|
330
|
+
"safety": SAFE_QUEUE_STATUS,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def queue_bundle_payload(
|
|
335
|
+
db_path: Path = DEFAULT_QUEUE_PATH,
|
|
336
|
+
*,
|
|
337
|
+
required_events: list[str] | None = None,
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
status = _redact_local_paths(queue_status_payload(db_path))
|
|
340
|
+
audit_summary = _redact_local_paths(
|
|
341
|
+
queue_audit_summary_payload(db_path, required_events=required_events)
|
|
342
|
+
)
|
|
343
|
+
work_items = _redact_local_paths([item.to_dict() for item in list_work_items(db_path=db_path)])
|
|
344
|
+
proposals = _redact_local_paths(
|
|
345
|
+
[proposal.to_dict() for proposal in list_proposals(db_path=db_path)]
|
|
346
|
+
)
|
|
347
|
+
audit_events = _redact_local_paths(export_audit_events(db_path=db_path)["audit_events"])
|
|
348
|
+
ready = audit_summary["status"] == "human_gates_exercised"
|
|
349
|
+
gate_summary = status["human_gate_summary"]
|
|
350
|
+
remaining_gate_gaps = audit_summary["missing_required_events"]
|
|
351
|
+
reviewer_summary = {
|
|
352
|
+
"status": "ready_for_reviewer_handoff" if ready else "needs_more_gate_evidence",
|
|
353
|
+
"ready_for_handoff": ready,
|
|
354
|
+
"human_gates_complete": ready,
|
|
355
|
+
"pending_decisions": gate_summary["total_pending_decisions"],
|
|
356
|
+
"approved_work_items": gate_summary["approved_work_items"],
|
|
357
|
+
"rejected_work_items": gate_summary["rejected_work_items"],
|
|
358
|
+
"approved_proposals": gate_summary["approved_proposals"],
|
|
359
|
+
"rejected_proposals": gate_summary["rejected_proposals"],
|
|
360
|
+
"remaining_gate_gaps": remaining_gate_gaps,
|
|
361
|
+
"review_steps": [
|
|
362
|
+
"Inspect work_items for local CI evidence and write_actions_allowed=false.",
|
|
363
|
+
"Inspect proposals for approved low-risk plans and rejected risky plans.",
|
|
364
|
+
"Inspect audit_summary for required human gate events.",
|
|
365
|
+
"Inspect safety to confirm the bundle is read-only and local paths are redacted.",
|
|
366
|
+
],
|
|
367
|
+
"execution_allowed": False,
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
"schema_version": QUEUE_BUNDLE_SCHEMA_VERSION,
|
|
371
|
+
"queue_schema_version": status["queue_schema_version"],
|
|
372
|
+
"patchrail_version": __version__,
|
|
373
|
+
"db_path": _redact_local_paths(str(db_path)),
|
|
374
|
+
"local_first": True,
|
|
375
|
+
"status": "ready_for_handoff" if ready else "needs_more_gate_evidence",
|
|
376
|
+
"counts": {
|
|
377
|
+
"work_items_total": len(work_items),
|
|
378
|
+
"proposals_total": len(proposals),
|
|
379
|
+
"audit_events_total": len(audit_events),
|
|
380
|
+
},
|
|
381
|
+
"status_summary": status,
|
|
382
|
+
"audit_summary": audit_summary,
|
|
383
|
+
"work_items": work_items,
|
|
384
|
+
"proposals": proposals,
|
|
385
|
+
"audit_events": audit_events,
|
|
386
|
+
"reviewer_summary": reviewer_summary,
|
|
387
|
+
"safety": {
|
|
388
|
+
**SAFE_QUEUE_STATUS,
|
|
389
|
+
"bundle_is_read_only": True,
|
|
390
|
+
"bundle_records_audit_event": False,
|
|
391
|
+
"local_paths_redacted": True,
|
|
392
|
+
},
|
|
393
|
+
"remaining_gate_gaps": remaining_gate_gaps,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def queue_gate_report_payload(
|
|
398
|
+
db_path: Path = DEFAULT_QUEUE_PATH,
|
|
399
|
+
*,
|
|
400
|
+
required_events: list[str] | None = None,
|
|
401
|
+
) -> dict[str, Any]:
|
|
402
|
+
status = _redact_local_paths(queue_status_payload(db_path))
|
|
403
|
+
audit_summary = _redact_local_paths(
|
|
404
|
+
queue_audit_summary_payload(db_path, required_events=required_events)
|
|
405
|
+
)
|
|
406
|
+
gate_summary = status["human_gate_summary"]
|
|
407
|
+
missing_required_events = audit_summary["missing_required_events"]
|
|
408
|
+
pending_decisions = gate_summary["total_pending_decisions"]
|
|
409
|
+
ready = pending_decisions == 0 and not missing_required_events
|
|
410
|
+
reviewer_actions: list[str] = []
|
|
411
|
+
if pending_decisions:
|
|
412
|
+
reviewer_actions.append("Review or reject all pending work items and proposals.")
|
|
413
|
+
if missing_required_events:
|
|
414
|
+
reviewer_actions.append("Exercise the missing local human-gate audit events.")
|
|
415
|
+
if not reviewer_actions:
|
|
416
|
+
reviewer_actions.append("Inspect the queue bundle before any separate write action.")
|
|
417
|
+
return {
|
|
418
|
+
"schema_version": QUEUE_GATE_REPORT_SCHEMA_VERSION,
|
|
419
|
+
"queue_schema_version": status["queue_schema_version"],
|
|
420
|
+
"patchrail_version": __version__,
|
|
421
|
+
"db_path": _redact_local_paths(str(db_path)),
|
|
422
|
+
"local_first": True,
|
|
423
|
+
"status": "ready_for_reviewer_handoff" if ready else "needs_reviewer_decisions",
|
|
424
|
+
"ready_for_reviewer_handoff": ready,
|
|
425
|
+
"pending_decisions": pending_decisions,
|
|
426
|
+
"missing_required_events": missing_required_events,
|
|
427
|
+
"decision_counts": {
|
|
428
|
+
"pending_work_items": gate_summary["pending_work_items"],
|
|
429
|
+
"pending_proposals": gate_summary["pending_proposals"],
|
|
430
|
+
"approved_work_items": gate_summary["approved_work_items"],
|
|
431
|
+
"rejected_work_items": gate_summary["rejected_work_items"],
|
|
432
|
+
"approved_proposals": gate_summary["approved_proposals"],
|
|
433
|
+
"rejected_proposals": gate_summary["rejected_proposals"],
|
|
434
|
+
},
|
|
435
|
+
"audit_counts": audit_summary["counts"],
|
|
436
|
+
"gates": audit_summary["gates"],
|
|
437
|
+
"reviewer_actions": reviewer_actions,
|
|
438
|
+
"safety": {
|
|
439
|
+
**SAFE_QUEUE_STATUS,
|
|
440
|
+
"report_is_read_only": True,
|
|
441
|
+
"report_records_audit_event": False,
|
|
442
|
+
"local_paths_redacted": True,
|
|
443
|
+
"execution_allowed": False,
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _compact_work_item(item: dict[str, Any]) -> dict[str, Any]:
|
|
449
|
+
payload = item.get("payload") if isinstance(item.get("payload"), dict) else {}
|
|
450
|
+
return {
|
|
451
|
+
"id": item["id"],
|
|
452
|
+
"kind": item["kind"],
|
|
453
|
+
"title": item["title"],
|
|
454
|
+
"source": item["source"],
|
|
455
|
+
"status": item["status"],
|
|
456
|
+
"approval_state": item["approval_state"],
|
|
457
|
+
"write_actions_allowed": item["write_actions_allowed"],
|
|
458
|
+
"decision_note": item.get("decision_note"),
|
|
459
|
+
"payload_keys": sorted(str(key) for key in payload),
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _compact_proposal(proposal: dict[str, Any]) -> dict[str, Any]:
|
|
464
|
+
return {
|
|
465
|
+
"id": proposal["id"],
|
|
466
|
+
"work_item_id": proposal["work_item_id"],
|
|
467
|
+
"title": proposal["title"],
|
|
468
|
+
"risk_level": proposal["risk_level"],
|
|
469
|
+
"approval_state": proposal["approval_state"],
|
|
470
|
+
"decision_note": proposal.get("decision_note"),
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _queue_review_handoff_checklist(
|
|
475
|
+
*,
|
|
476
|
+
pending_work_items: list[dict[str, Any]],
|
|
477
|
+
pending_proposals: list[dict[str, Any]],
|
|
478
|
+
) -> list[dict[str, str]]:
|
|
479
|
+
checklist: list[dict[str, str]] = []
|
|
480
|
+
if pending_work_items:
|
|
481
|
+
checklist.extend(
|
|
482
|
+
[
|
|
483
|
+
{
|
|
484
|
+
"state": "pending_work_items",
|
|
485
|
+
"command": "patchrail queue --db <queue.sqlite> approve <work-item-id>",
|
|
486
|
+
"purpose": "Approve a reviewed local work item without enabling execution.",
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"state": "pending_work_items",
|
|
490
|
+
"command": "patchrail queue --db <queue.sqlite> reject <work-item-id>",
|
|
491
|
+
"purpose": "Reject a local work item that should not move forward.",
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"state": "pending_work_items",
|
|
495
|
+
"command": (
|
|
496
|
+
"patchrail queue --db <queue.sqlite> skip <work-item-id> "
|
|
497
|
+
'--reason "revenue-safety policy: local-only human approval required"'
|
|
498
|
+
),
|
|
499
|
+
"purpose": "Skip retired or out-of-scope work while preserving history.",
|
|
500
|
+
},
|
|
501
|
+
]
|
|
502
|
+
)
|
|
503
|
+
if pending_proposals:
|
|
504
|
+
checklist.extend(
|
|
505
|
+
[
|
|
506
|
+
{
|
|
507
|
+
"state": "pending_proposals",
|
|
508
|
+
"command": (
|
|
509
|
+
"patchrail queue --db <queue.sqlite> proposal approve <proposal-id>"
|
|
510
|
+
),
|
|
511
|
+
"purpose": "Approve a reviewed local patch plan for maintainer handoff.",
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"state": "pending_proposals",
|
|
515
|
+
"command": (
|
|
516
|
+
"patchrail queue --db <queue.sqlite> proposal reject <proposal-id>"
|
|
517
|
+
),
|
|
518
|
+
"purpose": "Reject a patch plan that is risky, stale, or out of scope.",
|
|
519
|
+
},
|
|
520
|
+
]
|
|
521
|
+
)
|
|
522
|
+
if not checklist:
|
|
523
|
+
checklist.extend(
|
|
524
|
+
[
|
|
525
|
+
{
|
|
526
|
+
"state": "clear_for_handoff",
|
|
527
|
+
"command": "patchrail queue --db <queue.sqlite> policy-scan --format markdown",
|
|
528
|
+
"purpose": "Confirm no active policy-blocking records remain before handoff.",
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
"state": "clear_for_handoff",
|
|
532
|
+
"command": "patchrail queue --db <queue.sqlite> gate-report --format markdown",
|
|
533
|
+
"purpose": "Verify human gates and required local audit evidence.",
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"state": "clear_for_handoff",
|
|
537
|
+
"command": "patchrail queue --db <queue.sqlite> bundle --format markdown",
|
|
538
|
+
"purpose": "Generate the read-only reviewer handoff packet.",
|
|
539
|
+
},
|
|
540
|
+
]
|
|
541
|
+
)
|
|
542
|
+
return checklist
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def queue_review_payload(db_path: Path = DEFAULT_QUEUE_PATH) -> dict[str, Any]:
|
|
546
|
+
status = queue_status_payload(db_path)
|
|
547
|
+
work_items = _redact_local_paths([item.to_dict() for item in list_work_items(db_path=db_path)])
|
|
548
|
+
proposals = _redact_local_paths(
|
|
549
|
+
[proposal.to_dict() for proposal in list_proposals(db_path=db_path)]
|
|
550
|
+
)
|
|
551
|
+
pending_work_items = [
|
|
552
|
+
_compact_work_item(item) for item in work_items if item["approval_state"] == "pending"
|
|
553
|
+
]
|
|
554
|
+
pending_proposals = [
|
|
555
|
+
_compact_proposal(proposal)
|
|
556
|
+
for proposal in proposals
|
|
557
|
+
if proposal["approval_state"] == "pending"
|
|
558
|
+
]
|
|
559
|
+
approved_work_items = [
|
|
560
|
+
_compact_work_item(item) for item in work_items if item["approval_state"] == "approved"
|
|
561
|
+
]
|
|
562
|
+
approved_proposals = [
|
|
563
|
+
_compact_proposal(proposal)
|
|
564
|
+
for proposal in proposals
|
|
565
|
+
if proposal["approval_state"] == "approved"
|
|
566
|
+
]
|
|
567
|
+
rejected_work_items = [
|
|
568
|
+
_compact_work_item(item) for item in work_items if item["approval_state"] == "rejected"
|
|
569
|
+
]
|
|
570
|
+
rejected_proposals = [
|
|
571
|
+
_compact_proposal(proposal)
|
|
572
|
+
for proposal in proposals
|
|
573
|
+
if proposal["approval_state"] == "rejected"
|
|
574
|
+
]
|
|
575
|
+
pending_decisions = len(pending_work_items) + len(pending_proposals)
|
|
576
|
+
reviewer_actions: list[str] = []
|
|
577
|
+
if pending_work_items:
|
|
578
|
+
reviewer_actions.append("Review pending work items, then approve, reject, or skip them.")
|
|
579
|
+
if pending_proposals:
|
|
580
|
+
reviewer_actions.append("Review pending proposals, then approve or reject each local plan.")
|
|
581
|
+
if not reviewer_actions:
|
|
582
|
+
reviewer_actions.append("No pending decisions remain; inspect gate-report or bundle next.")
|
|
583
|
+
handoff_checklist = _queue_review_handoff_checklist(
|
|
584
|
+
pending_work_items=pending_work_items,
|
|
585
|
+
pending_proposals=pending_proposals,
|
|
586
|
+
)
|
|
587
|
+
return {
|
|
588
|
+
"schema_version": QUEUE_REVIEW_SCHEMA_VERSION,
|
|
589
|
+
"queue_schema_version": status["queue_schema_version"],
|
|
590
|
+
"patchrail_version": __version__,
|
|
591
|
+
"db_path": _redact_local_paths(str(db_path)),
|
|
592
|
+
"local_first": True,
|
|
593
|
+
"status": "awaiting_human_review" if pending_decisions else "clear_for_handoff",
|
|
594
|
+
"ready_for_reviewer_handoff": pending_decisions == 0,
|
|
595
|
+
"pending_decisions": pending_decisions,
|
|
596
|
+
"counts": status["counts"],
|
|
597
|
+
"review_groups": {
|
|
598
|
+
"pending_work_items": pending_work_items,
|
|
599
|
+
"pending_proposals": pending_proposals,
|
|
600
|
+
"approved_work_items": approved_work_items,
|
|
601
|
+
"approved_proposals": approved_proposals,
|
|
602
|
+
"rejected_work_items": rejected_work_items,
|
|
603
|
+
"rejected_proposals": rejected_proposals,
|
|
604
|
+
},
|
|
605
|
+
"reviewer_actions": reviewer_actions,
|
|
606
|
+
"handoff_checklist": handoff_checklist,
|
|
607
|
+
"safety": {
|
|
608
|
+
**SAFE_QUEUE_STATUS,
|
|
609
|
+
"review_is_read_only": True,
|
|
610
|
+
"review_records_audit_event": False,
|
|
611
|
+
"local_paths_redacted": True,
|
|
612
|
+
"execution_allowed": False,
|
|
613
|
+
},
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def queue_policy_scan_payload(db_path: Path = DEFAULT_QUEUE_PATH) -> dict[str, Any]:
|
|
618
|
+
status = queue_status_payload(db_path)
|
|
619
|
+
work_items = _redact_local_paths([item.to_dict() for item in list_work_items(db_path=db_path)])
|
|
620
|
+
proposals = _redact_local_paths(
|
|
621
|
+
[proposal.to_dict() for proposal in list_proposals(db_path=db_path)]
|
|
622
|
+
)
|
|
623
|
+
matches = [
|
|
624
|
+
*(match for item in work_items if (match := _policy_scan_work_item(item)) is not None),
|
|
625
|
+
*(
|
|
626
|
+
match
|
|
627
|
+
for proposal in proposals
|
|
628
|
+
if (match := _policy_scan_proposal(proposal)) is not None
|
|
629
|
+
),
|
|
630
|
+
]
|
|
631
|
+
reviewer_actions = (
|
|
632
|
+
[
|
|
633
|
+
"Reject or skip matching local records before any handoff.",
|
|
634
|
+
"Keep historical records visible; do not delete queue data to hide policy drift.",
|
|
635
|
+
]
|
|
636
|
+
if matches
|
|
637
|
+
else ["No policy-blocking queue records found; continue with gate-report or bundle."]
|
|
638
|
+
)
|
|
639
|
+
return {
|
|
640
|
+
"schema_version": QUEUE_POLICY_SCAN_SCHEMA_VERSION,
|
|
641
|
+
"queue_schema_version": status["queue_schema_version"],
|
|
642
|
+
"patchrail_version": __version__,
|
|
643
|
+
"db_path": _redact_local_paths(str(db_path)),
|
|
644
|
+
"local_first": True,
|
|
645
|
+
"status": "blocked_records_present" if matches else "policy_clear",
|
|
646
|
+
"blocked_records_count": len(matches),
|
|
647
|
+
"scanned_counts": {
|
|
648
|
+
"work_items_total": len(work_items),
|
|
649
|
+
"proposals_total": len(proposals),
|
|
650
|
+
"audit_events_total": status["counts"]["audit_events_total"],
|
|
651
|
+
},
|
|
652
|
+
"policy_categories": sorted(POLICY_SCAN_RULES),
|
|
653
|
+
"matches": matches,
|
|
654
|
+
"reviewer_actions": reviewer_actions,
|
|
655
|
+
"safety": {
|
|
656
|
+
**SAFE_QUEUE_STATUS,
|
|
657
|
+
"scan_is_read_only": True,
|
|
658
|
+
"scan_records_audit_event": False,
|
|
659
|
+
"local_paths_redacted": True,
|
|
660
|
+
"execution_allowed": False,
|
|
661
|
+
},
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def queue_policy_resolution_payload(
|
|
666
|
+
db_path: Path = DEFAULT_QUEUE_PATH,
|
|
667
|
+
*,
|
|
668
|
+
reason: str = DEFAULT_POLICY_RESOLUTION_REASON,
|
|
669
|
+
) -> dict[str, Any]:
|
|
670
|
+
before = queue_policy_scan_payload(db_path)
|
|
671
|
+
resolved_records: list[dict[str, Any]] = []
|
|
672
|
+
work_items_skipped = 0
|
|
673
|
+
proposals_rejected = 0
|
|
674
|
+
audit_before = before["scanned_counts"]["audit_events_total"]
|
|
675
|
+
|
|
676
|
+
for match in before["matches"]:
|
|
677
|
+
if match["record_type"] == "work_item":
|
|
678
|
+
item = skip_work_item(
|
|
679
|
+
db_path=db_path,
|
|
680
|
+
item_id=match["id"],
|
|
681
|
+
decision_note=reason,
|
|
682
|
+
).to_dict()
|
|
683
|
+
item = _redact_local_paths(item)
|
|
684
|
+
work_items_skipped += 1
|
|
685
|
+
resolved_records.append(
|
|
686
|
+
{
|
|
687
|
+
"record_type": "work_item",
|
|
688
|
+
"id": match["id"],
|
|
689
|
+
"title": match["title"],
|
|
690
|
+
"action": "skipped",
|
|
691
|
+
"approval_state_after": item["approval_state"],
|
|
692
|
+
"status_after": item["status"],
|
|
693
|
+
"matched_categories": match["matched_categories"],
|
|
694
|
+
"matched_terms": match["matched_terms"],
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
elif match["record_type"] == "proposal":
|
|
698
|
+
proposal = reject_proposal(
|
|
699
|
+
db_path=db_path,
|
|
700
|
+
proposal_id=match["id"],
|
|
701
|
+
decision_note=reason,
|
|
702
|
+
).to_dict()
|
|
703
|
+
proposal = _redact_local_paths(proposal)
|
|
704
|
+
proposals_rejected += 1
|
|
705
|
+
resolved_records.append(
|
|
706
|
+
{
|
|
707
|
+
"record_type": "proposal",
|
|
708
|
+
"id": match["id"],
|
|
709
|
+
"work_item_id": match["work_item_id"],
|
|
710
|
+
"title": match["title"],
|
|
711
|
+
"action": "rejected",
|
|
712
|
+
"approval_state_after": proposal["approval_state"],
|
|
713
|
+
"status_after": "rejected",
|
|
714
|
+
"matched_categories": match["matched_categories"],
|
|
715
|
+
"matched_terms": match["matched_terms"],
|
|
716
|
+
}
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
after = queue_policy_scan_payload(db_path)
|
|
720
|
+
audit_after = after["scanned_counts"]["audit_events_total"]
|
|
721
|
+
return {
|
|
722
|
+
"schema_version": QUEUE_POLICY_RESOLUTION_SCHEMA_VERSION,
|
|
723
|
+
"queue_schema_version": before["queue_schema_version"],
|
|
724
|
+
"patchrail_version": __version__,
|
|
725
|
+
"db_path": _redact_local_paths(str(db_path)),
|
|
726
|
+
"local_first": True,
|
|
727
|
+
"status": "resolved_blocked_records" if resolved_records else "no_blocked_records",
|
|
728
|
+
"reason": reason,
|
|
729
|
+
"resolved_records_count": len(resolved_records),
|
|
730
|
+
"resolved_counts": {
|
|
731
|
+
"work_items_skipped": work_items_skipped,
|
|
732
|
+
"proposals_rejected": proposals_rejected,
|
|
733
|
+
"audit_events_added": audit_after - audit_before,
|
|
734
|
+
},
|
|
735
|
+
"resolved_records": resolved_records,
|
|
736
|
+
"before_policy_status": before["status"],
|
|
737
|
+
"after_policy_status": after["status"],
|
|
738
|
+
"remaining_blocked_records_count": after["blocked_records_count"],
|
|
739
|
+
"reviewer_actions": [
|
|
740
|
+
"Run policy-scan again before handoff.",
|
|
741
|
+
"Inspect audit events to confirm skipped work items and rejected proposals remain visible.",
|
|
742
|
+
]
|
|
743
|
+
if resolved_records
|
|
744
|
+
else ["No policy-blocking queue records were active."],
|
|
745
|
+
"safety": {
|
|
746
|
+
**SAFE_QUEUE_STATUS,
|
|
747
|
+
"resolution_is_local_only": True,
|
|
748
|
+
"resolution_records_audit_event": True,
|
|
749
|
+
"local_paths_redacted": True,
|
|
750
|
+
"execution_allowed": False,
|
|
751
|
+
"github_write_performed": False,
|
|
752
|
+
"network_performed": False,
|
|
753
|
+
"proposals_executed": False,
|
|
754
|
+
"work_items_deleted": False,
|
|
755
|
+
},
|
|
756
|
+
}
|