code-mower 0.5.0b5__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.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Draft calibration corpus discovery from GitHub PR metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as _dt
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping, Sequence
|
|
11
|
+
|
|
12
|
+
from .corpus import parse_int
|
|
13
|
+
from .identity import safe_slug
|
|
14
|
+
from .run_status import RUN_STATUS_BLOCKED, RUN_STATUS_PASS, RUN_STATUS_UNKNOWN
|
|
15
|
+
|
|
16
|
+
AUTO_DISCOVERY_SCHEMA = "code_mower.calibrationAutoDiscover.v1"
|
|
17
|
+
TRUTH_EXPECTATION_UNKNOWN = "unknown"
|
|
18
|
+
TRUTH_EXPECTATION_KNOWN_CLEAN = "known_clean"
|
|
19
|
+
TRUTH_EXPECTATION_KNOWN_BLOCKED = "known_blocked"
|
|
20
|
+
|
|
21
|
+
AUDIT_STATE_RE = re.compile(
|
|
22
|
+
r"<!--\s*(?P<provider>[A-Z0-9_]+)_AUDIT_STATE:\s*"
|
|
23
|
+
r"(?P<state>[a-z0-9_-]+)\s*-->",
|
|
24
|
+
re.IGNORECASE,
|
|
25
|
+
)
|
|
26
|
+
FINDINGS_COUNT_RE = re.compile(
|
|
27
|
+
r"Findings:\s*P0=(?P<p0>\d+),\s*P1=(?P<p1>\d+),\s*"
|
|
28
|
+
r"P2=(?P<p2>\d+),\s*P3=(?P<p3>\d+)",
|
|
29
|
+
re.IGNORECASE,
|
|
30
|
+
)
|
|
31
|
+
HEAD_SHA_RE = re.compile(r"Head SHA:\s*`?(?P<head>[0-9a-f]{7,40})`?", re.IGNORECASE)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def gh_pr_list_fields() -> str:
|
|
35
|
+
return ",".join(
|
|
36
|
+
[
|
|
37
|
+
"number",
|
|
38
|
+
"title",
|
|
39
|
+
"headRefOid",
|
|
40
|
+
"baseRefName",
|
|
41
|
+
"mergedAt",
|
|
42
|
+
"changedFiles",
|
|
43
|
+
"comments",
|
|
44
|
+
"reviews",
|
|
45
|
+
"labels",
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_auto_discovery_input(path: Path) -> list[dict[str, Any]]:
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
53
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
54
|
+
raise ValueError(f"unable to read {path}: {exc}") from exc
|
|
55
|
+
raw_items = (
|
|
56
|
+
payload.get("pull_requests", payload.get("prs", payload))
|
|
57
|
+
if isinstance(payload, Mapping)
|
|
58
|
+
else payload
|
|
59
|
+
)
|
|
60
|
+
if not isinstance(raw_items, list):
|
|
61
|
+
raise ValueError("auto-discover input must be a GitHub PR list JSON array")
|
|
62
|
+
items: list[dict[str, Any]] = []
|
|
63
|
+
for index, item in enumerate(raw_items):
|
|
64
|
+
if not isinstance(item, Mapping):
|
|
65
|
+
raise ValueError(f"auto-discover input[{index}] must be a JSON object")
|
|
66
|
+
items.append(dict(item))
|
|
67
|
+
return items
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def fetch_merged_prs_for_auto_discovery(
|
|
71
|
+
*,
|
|
72
|
+
repo: str,
|
|
73
|
+
last_n: int,
|
|
74
|
+
runner: Any = subprocess.run,
|
|
75
|
+
) -> list[dict[str, Any]]:
|
|
76
|
+
if last_n < 1:
|
|
77
|
+
raise ValueError("--last-n must be at least 1")
|
|
78
|
+
completed = runner(
|
|
79
|
+
[
|
|
80
|
+
"gh",
|
|
81
|
+
"pr",
|
|
82
|
+
"list",
|
|
83
|
+
"--repo",
|
|
84
|
+
repo,
|
|
85
|
+
"--state",
|
|
86
|
+
"merged",
|
|
87
|
+
"--limit",
|
|
88
|
+
str(last_n),
|
|
89
|
+
"--json",
|
|
90
|
+
gh_pr_list_fields(),
|
|
91
|
+
],
|
|
92
|
+
text=True,
|
|
93
|
+
capture_output=True,
|
|
94
|
+
check=False,
|
|
95
|
+
)
|
|
96
|
+
if completed.returncode != 0:
|
|
97
|
+
stderr = str(getattr(completed, "stderr", "") or "").strip()
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"gh pr list failed for calibration auto-discovery"
|
|
100
|
+
+ (f": {stderr}" if stderr else "")
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
payload = json.loads(str(getattr(completed, "stdout", "") or ""))
|
|
104
|
+
except json.JSONDecodeError as exc:
|
|
105
|
+
raise ValueError(f"gh pr list returned invalid JSON: {exc}") from exc
|
|
106
|
+
if not isinstance(payload, list):
|
|
107
|
+
raise ValueError("gh pr list returned a non-list payload")
|
|
108
|
+
return [dict(item) for item in payload if isinstance(item, Mapping)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _provider_from_audit_state(provider: str, state: str) -> str:
|
|
112
|
+
provider_slug = provider.lower().replace("_", "-")
|
|
113
|
+
if provider_slug in {"codex", "claude", "devin", "gitar"}:
|
|
114
|
+
return f"{provider_slug}-audit"
|
|
115
|
+
state_parts = state.split("-audit-", 1)
|
|
116
|
+
if len(state_parts) == 2 and state_parts[0]:
|
|
117
|
+
return f"{state_parts[0]}-audit"
|
|
118
|
+
return f"{provider_slug}-audit"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _audit_status_from_state(state: str) -> str:
|
|
122
|
+
normalized = state.lower()
|
|
123
|
+
if normalized.endswith("-done") or normalized in {"done", "pass", "passed"}:
|
|
124
|
+
return RUN_STATUS_PASS
|
|
125
|
+
if normalized.endswith("-blocked") or normalized in {"blocked", "fail", "failed"}:
|
|
126
|
+
return RUN_STATUS_BLOCKED
|
|
127
|
+
return RUN_STATUS_UNKNOWN
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _finding_count_from_comment(body: str) -> int:
|
|
131
|
+
match = FINDINGS_COUNT_RE.search(body)
|
|
132
|
+
if not match:
|
|
133
|
+
return 0
|
|
134
|
+
return sum(int(match.group(name)) for name in ("p0", "p1", "p2", "p3"))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _head_sha_from_comment(body: str) -> str:
|
|
138
|
+
match = HEAD_SHA_RE.search(body)
|
|
139
|
+
return match.group("head") if match else ""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _audit_runs_from_comments(comments: Sequence[Any]) -> list[dict[str, Any]]:
|
|
143
|
+
runs: list[dict[str, Any]] = []
|
|
144
|
+
for comment in comments:
|
|
145
|
+
if not isinstance(comment, Mapping):
|
|
146
|
+
continue
|
|
147
|
+
body = str(comment.get("body") or "")
|
|
148
|
+
if not body:
|
|
149
|
+
continue
|
|
150
|
+
for match in AUDIT_STATE_RE.finditer(body):
|
|
151
|
+
state = match.group("state")
|
|
152
|
+
reviewer = _provider_from_audit_state(match.group("provider"), state)
|
|
153
|
+
status = _audit_status_from_state(state)
|
|
154
|
+
run: dict[str, Any] = {
|
|
155
|
+
"reviewer": reviewer,
|
|
156
|
+
"status": status,
|
|
157
|
+
"finding_count": _finding_count_from_comment(body),
|
|
158
|
+
"source": "github-comment-trailer",
|
|
159
|
+
}
|
|
160
|
+
head_sha = _head_sha_from_comment(body)
|
|
161
|
+
if head_sha:
|
|
162
|
+
run["head_sha"] = head_sha
|
|
163
|
+
if status == RUN_STATUS_BLOCKED:
|
|
164
|
+
run["expected_blocker_caught"] = True
|
|
165
|
+
run["disposition"] = "true_positive"
|
|
166
|
+
runs.append(run)
|
|
167
|
+
return runs
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _review_signal_count(reviews: Sequence[Any], comments: Sequence[Any]) -> int:
|
|
171
|
+
count = 0
|
|
172
|
+
for review in reviews:
|
|
173
|
+
if not isinstance(review, Mapping):
|
|
174
|
+
continue
|
|
175
|
+
state = str(review.get("state") or "").upper()
|
|
176
|
+
if state == "CHANGES_REQUESTED":
|
|
177
|
+
count += 1
|
|
178
|
+
for comment in comments:
|
|
179
|
+
if not isinstance(comment, Mapping):
|
|
180
|
+
continue
|
|
181
|
+
body = str(comment.get("body") or "").lower()
|
|
182
|
+
if "changes requested" in body or "requested changes" in body:
|
|
183
|
+
count += 1
|
|
184
|
+
return count
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _difficulty_from_changed_files(value: Any) -> str:
|
|
188
|
+
changed_files = parse_int(value or 0, field="changedFiles")
|
|
189
|
+
if changed_files >= 20:
|
|
190
|
+
return "hard"
|
|
191
|
+
if changed_files >= 6:
|
|
192
|
+
return "medium"
|
|
193
|
+
return "easy"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_auto_discovered_corpus(
|
|
197
|
+
*,
|
|
198
|
+
repo: str,
|
|
199
|
+
pull_requests: Sequence[Mapping[str, Any]],
|
|
200
|
+
last_n: int,
|
|
201
|
+
) -> dict[str, Any]:
|
|
202
|
+
if "/" not in repo:
|
|
203
|
+
raise ValueError("--repo must be an owner/repo slug")
|
|
204
|
+
items: list[dict[str, Any]] = []
|
|
205
|
+
for raw in pull_requests:
|
|
206
|
+
pr_number = parse_int(raw.get("number"), field="pull_request.number")
|
|
207
|
+
final_head_sha = str(raw.get("headRefOid") or "")
|
|
208
|
+
comments = list(raw.get("comments", []) or [])
|
|
209
|
+
reviews = list(raw.get("reviews", []) or [])
|
|
210
|
+
reviewer_runs = _audit_runs_from_comments(comments)
|
|
211
|
+
blocked_runs = [
|
|
212
|
+
run for run in reviewer_runs if run.get("status") == RUN_STATUS_BLOCKED
|
|
213
|
+
]
|
|
214
|
+
pass_runs = [
|
|
215
|
+
run for run in reviewer_runs if run.get("status") == RUN_STATUS_PASS
|
|
216
|
+
]
|
|
217
|
+
review_signals = _review_signal_count(reviews, comments)
|
|
218
|
+
shared_signal_summary = {
|
|
219
|
+
"audit_blocked_runs": len(blocked_runs),
|
|
220
|
+
"review_signal_count": review_signals,
|
|
221
|
+
"audit_run_count": len(reviewer_runs),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def make_item(
|
|
225
|
+
*,
|
|
226
|
+
head_sha: str,
|
|
227
|
+
source: str,
|
|
228
|
+
expectation: str,
|
|
229
|
+
runs: list[dict[str, Any]],
|
|
230
|
+
signal_summary: Mapping[str, Any],
|
|
231
|
+
) -> dict[str, Any]:
|
|
232
|
+
return {
|
|
233
|
+
"repo": repo,
|
|
234
|
+
"pr_number": pr_number,
|
|
235
|
+
"head_sha": head_sha,
|
|
236
|
+
"base_ref": str(raw.get("baseRefName") or ""),
|
|
237
|
+
"difficulty": _difficulty_from_changed_files(raw.get("changedFiles")),
|
|
238
|
+
"review_class": "auto-discovered",
|
|
239
|
+
"source": source,
|
|
240
|
+
"truth": {
|
|
241
|
+
"expectation": expectation,
|
|
242
|
+
"notes": (
|
|
243
|
+
"Draft auto-discovered from merged PR metadata. Confirm this "
|
|
244
|
+
"disposition before using it for lane promotion or merge policy."
|
|
245
|
+
),
|
|
246
|
+
},
|
|
247
|
+
"expected_findings": [],
|
|
248
|
+
"reviewer_runs": runs,
|
|
249
|
+
"notes": (
|
|
250
|
+
f"PR title: {raw.get('title') or ''}. "
|
|
251
|
+
f"Discovery signals: {json.dumps(dict(signal_summary), sort_keys=True)}"
|
|
252
|
+
),
|
|
253
|
+
"auto_discovery": dict(signal_summary),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
blocked_runs_by_head: dict[str, list[dict[str, Any]]] = {}
|
|
257
|
+
for run in blocked_runs:
|
|
258
|
+
head_sha = str(run.get("head_sha") or "")
|
|
259
|
+
blocked_runs_by_head.setdefault(head_sha, []).append(run)
|
|
260
|
+
|
|
261
|
+
for head_sha, runs in sorted(blocked_runs_by_head.items()):
|
|
262
|
+
signal_summary = {
|
|
263
|
+
**shared_signal_summary,
|
|
264
|
+
"case": "historical-blocked-head",
|
|
265
|
+
"case_head_sha": head_sha,
|
|
266
|
+
}
|
|
267
|
+
items.append(
|
|
268
|
+
make_item(
|
|
269
|
+
head_sha=head_sha,
|
|
270
|
+
source="auto-discovered-structured-blocker",
|
|
271
|
+
expectation=TRUTH_EXPECTATION_KNOWN_BLOCKED,
|
|
272
|
+
runs=runs,
|
|
273
|
+
signal_summary=signal_summary,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
final_pass_runs = [
|
|
278
|
+
run
|
|
279
|
+
for run in pass_runs
|
|
280
|
+
if not run.get("head_sha") or run.get("head_sha") == final_head_sha
|
|
281
|
+
]
|
|
282
|
+
explicit_blocked_heads = {
|
|
283
|
+
str(run.get("head_sha")) for run in blocked_runs if run.get("head_sha")
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
final_head_sha
|
|
287
|
+
and final_head_sha not in explicit_blocked_heads
|
|
288
|
+
and (not blocked_runs or final_pass_runs)
|
|
289
|
+
):
|
|
290
|
+
signal_summary = {
|
|
291
|
+
**shared_signal_summary,
|
|
292
|
+
"case": "merged-final-head",
|
|
293
|
+
"case_head_sha": final_head_sha,
|
|
294
|
+
}
|
|
295
|
+
items.append(
|
|
296
|
+
make_item(
|
|
297
|
+
head_sha=final_head_sha,
|
|
298
|
+
source=(
|
|
299
|
+
"auto-discovered-merged-clean-after-fix"
|
|
300
|
+
if blocked_runs
|
|
301
|
+
else "auto-discovered-merged-clean"
|
|
302
|
+
),
|
|
303
|
+
expectation=TRUTH_EXPECTATION_KNOWN_CLEAN
|
|
304
|
+
if not review_signals or blocked_runs
|
|
305
|
+
else TRUTH_EXPECTATION_UNKNOWN,
|
|
306
|
+
runs=final_pass_runs,
|
|
307
|
+
signal_summary=signal_summary,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
elif review_signals and not blocked_runs:
|
|
311
|
+
signal_summary = {
|
|
312
|
+
**shared_signal_summary,
|
|
313
|
+
"case": "review-signal-needs-human-disposition",
|
|
314
|
+
"case_head_sha": final_head_sha,
|
|
315
|
+
}
|
|
316
|
+
items.append(
|
|
317
|
+
make_item(
|
|
318
|
+
head_sha=final_head_sha,
|
|
319
|
+
source="auto-discovered-review-signal-needs-human-disposition",
|
|
320
|
+
expectation=TRUTH_EXPECTATION_UNKNOWN,
|
|
321
|
+
runs=[],
|
|
322
|
+
signal_summary=signal_summary,
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
return {
|
|
326
|
+
"version": 1,
|
|
327
|
+
"name": f"auto-discovered-{safe_slug(repo)}",
|
|
328
|
+
"description": (
|
|
329
|
+
"Draft calibration corpus generated from recent merged GitHub PRs. "
|
|
330
|
+
"Review every disposition before treating it as benchmark truth."
|
|
331
|
+
),
|
|
332
|
+
"discovery": {
|
|
333
|
+
"schema": AUTO_DISCOVERY_SCHEMA,
|
|
334
|
+
"repo": repo,
|
|
335
|
+
"last_n": last_n,
|
|
336
|
+
"generated_at": _dt.datetime.now(tz=_dt.timezone.utc)
|
|
337
|
+
.replace(microsecond=0)
|
|
338
|
+
.isoformat()
|
|
339
|
+
.replace("+00:00", "Z"),
|
|
340
|
+
"source": "gh pr list --state merged",
|
|
341
|
+
"caveat": (
|
|
342
|
+
"Known-clean and known-blocked labels are heuristics from merged "
|
|
343
|
+
"PR history, structured audit trailers, and review request signals. "
|
|
344
|
+
"They are starter dispositions, not automatic merge-gating truth."
|
|
345
|
+
),
|
|
346
|
+
},
|
|
347
|
+
"corpus": items,
|
|
348
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Calibration command materialization helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
from .identity import safe_slug
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_repo_path_map(entries: Sequence[str]) -> dict[str, str]:
|
|
13
|
+
mapping: dict[str, str] = {}
|
|
14
|
+
for entry in entries:
|
|
15
|
+
if "=" not in entry:
|
|
16
|
+
raise ValueError(
|
|
17
|
+
"repo path map entries must be OWNER/REPO=PATH, "
|
|
18
|
+
f"OWNER/REPO#PR=PATH, or OWNER/REPO@HEAD=PATH: {entry}"
|
|
19
|
+
)
|
|
20
|
+
selector, path = entry.split("=", 1)
|
|
21
|
+
selector = selector.strip()
|
|
22
|
+
path = path.strip()
|
|
23
|
+
repo = re.split(r"[#@]", selector, maxsplit=1)[0]
|
|
24
|
+
if "/" not in repo or not path:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"repo path map entries must be OWNER/REPO=PATH, "
|
|
27
|
+
f"OWNER/REPO#PR=PATH, or OWNER/REPO@HEAD=PATH: {entry}"
|
|
28
|
+
)
|
|
29
|
+
mapping[selector] = path
|
|
30
|
+
return mapping
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def repo_path_for_item(item: Mapping[str, Any], repo_path_map: Mapping[str, str]) -> str:
|
|
34
|
+
repo = str(item.get("repo") or "")
|
|
35
|
+
pr_number = str(item.get("pr_number") or "")
|
|
36
|
+
head_sha = str(item.get("head_sha") or "")
|
|
37
|
+
selectors = [
|
|
38
|
+
f"{repo}#{pr_number}@{head_sha}" if repo and pr_number and head_sha else "",
|
|
39
|
+
f"{repo}#{pr_number}" if repo and pr_number else "",
|
|
40
|
+
f"{repo}@{head_sha}" if repo and head_sha else "",
|
|
41
|
+
repo,
|
|
42
|
+
]
|
|
43
|
+
for selector in selectors:
|
|
44
|
+
if selector and selector in repo_path_map:
|
|
45
|
+
return repo_path_map[selector]
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def command_lane_id(command: Sequence[Any]) -> str:
|
|
50
|
+
parts = [str(part) for part in command]
|
|
51
|
+
if len(parts) >= 2 and parts[0] == "code-mower":
|
|
52
|
+
if parts[1] == "local-llm" and len(parts) >= 3 and parts[2] == "bakeoff":
|
|
53
|
+
return "local-llm"
|
|
54
|
+
return parts[1].replace("_", "-")
|
|
55
|
+
return safe_slug(parts[0] if parts else "command", "command")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def option_value(command: Sequence[str], option: str) -> str:
|
|
59
|
+
for index, part in enumerate(command):
|
|
60
|
+
if part == option and index + 1 < len(command):
|
|
61
|
+
return command[index + 1]
|
|
62
|
+
if part.startswith(f"{option}="):
|
|
63
|
+
return part.split("=", 1)[1]
|
|
64
|
+
return ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def reviewer_id_from_command(command: Sequence[Any]) -> str:
|
|
68
|
+
lane_id = command_lane_id(command)
|
|
69
|
+
output_dir = option_value([str(part) for part in command], "--output-dir")
|
|
70
|
+
output_leaf = safe_slug(Path(output_dir).name if output_dir else "", "")
|
|
71
|
+
default_leaf = {
|
|
72
|
+
"antigravity-cli": "antigravity-cli",
|
|
73
|
+
"gemini-cli": "gemini-cli",
|
|
74
|
+
"hermes-cli": "hermes-cli",
|
|
75
|
+
"coderabbit-cli": "coderabbit-cli",
|
|
76
|
+
"local-llm": "local-llm",
|
|
77
|
+
}.get(lane_id, lane_id)
|
|
78
|
+
if output_leaf and output_leaf != default_leaf:
|
|
79
|
+
return output_leaf
|
|
80
|
+
return lane_id
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def command_metadata_for_run(run: Mapping[str, Any], command_index: int) -> dict[str, Any]:
|
|
84
|
+
command_metadata = run.get("command_metadata", [])
|
|
85
|
+
if (
|
|
86
|
+
isinstance(command_metadata, list)
|
|
87
|
+
and 0 <= command_index < len(command_metadata)
|
|
88
|
+
and isinstance(command_metadata[command_index], Mapping)
|
|
89
|
+
):
|
|
90
|
+
return dict(command_metadata[command_index])
|
|
91
|
+
return {}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def local_llm_profiles_from_command(command: Sequence[Any]) -> list[str]:
|
|
95
|
+
profiles = option_value([str(part) for part in command], "--profiles")
|
|
96
|
+
return [profile.strip() for profile in profiles.split(",") if profile.strip()]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def set_option_value(command: list[str], option: str, value: str) -> None:
|
|
100
|
+
for index, part in enumerate(command):
|
|
101
|
+
if part == option and index + 1 < len(command):
|
|
102
|
+
command[index + 1] = value
|
|
103
|
+
return
|
|
104
|
+
if part.startswith(f"{option}="):
|
|
105
|
+
command[index] = f"{option}={value}"
|
|
106
|
+
return
|
|
107
|
+
command.extend([option, value])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def has_flag(command: Sequence[str], flag: str) -> bool:
|
|
111
|
+
return any(part == flag for part in command)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def rewrite_code_mower_command(
|
|
115
|
+
command: Sequence[Any],
|
|
116
|
+
*,
|
|
117
|
+
code_mower_command: Sequence[str],
|
|
118
|
+
) -> list[str]:
|
|
119
|
+
parts = [str(part) for part in command]
|
|
120
|
+
if parts and parts[0] == "code-mower":
|
|
121
|
+
return [*code_mower_command, *parts[1:]]
|
|
122
|
+
return parts
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def materialize_command(
|
|
126
|
+
command: Sequence[Any],
|
|
127
|
+
*,
|
|
128
|
+
item: Mapping[str, Any],
|
|
129
|
+
code_mower_command: Sequence[str],
|
|
130
|
+
repo_path_map: Mapping[str, str],
|
|
131
|
+
allow_historical_head: bool,
|
|
132
|
+
) -> list[str]:
|
|
133
|
+
materialized = rewrite_code_mower_command(
|
|
134
|
+
command,
|
|
135
|
+
code_mower_command=code_mower_command,
|
|
136
|
+
)
|
|
137
|
+
lane_id = command_lane_id(command)
|
|
138
|
+
repo = str(item.get("repo") or "")
|
|
139
|
+
repo_path = repo_path_for_item(item, repo_path_map)
|
|
140
|
+
historical_local_cli_lanes = {"antigravity-cli", "gemini-cli", "hermes-cli"}
|
|
141
|
+
if lane_id in {"coderabbit-cli", "local-llm", *historical_local_cli_lanes}:
|
|
142
|
+
existing_repo_path = option_value(materialized, "--repo-path")
|
|
143
|
+
if repo_path:
|
|
144
|
+
set_option_value(materialized, "--repo-path", repo_path)
|
|
145
|
+
if lane_id in {"coderabbit-cli", "local-llm", *historical_local_cli_lanes} and allow_historical_head:
|
|
146
|
+
if not has_flag(materialized, "--allow-historical-head"):
|
|
147
|
+
materialized.append("--allow-historical-head")
|
|
148
|
+
if lane_id in historical_local_cli_lanes and not has_flag(
|
|
149
|
+
materialized,
|
|
150
|
+
"--historical-calibration",
|
|
151
|
+
):
|
|
152
|
+
materialized.append("--historical-calibration")
|
|
153
|
+
elif existing_repo_path == "/path/to/pr-worktree":
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"{lane_id} for {repo} needs --repo-path-map {repo}=/path/to/pr-worktree"
|
|
156
|
+
)
|
|
157
|
+
return materialized
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def summary_path_for_command(command: Sequence[Any]) -> Path | None:
|
|
161
|
+
lane_id = command_lane_id(command)
|
|
162
|
+
output_dir = option_value([str(part) for part in command], "--output-dir")
|
|
163
|
+
if not output_dir:
|
|
164
|
+
return None
|
|
165
|
+
root = Path(output_dir)
|
|
166
|
+
if lane_id == "local-llm":
|
|
167
|
+
return root / "summary.json"
|
|
168
|
+
if lane_id == "antigravity-cli":
|
|
169
|
+
return root / "antigravity-cli.summary.json"
|
|
170
|
+
if lane_id == "gemini-cli":
|
|
171
|
+
return root / "gemini-cli.summary.json"
|
|
172
|
+
if lane_id == "hermes-cli":
|
|
173
|
+
return root / "hermes-cli.summary.json"
|
|
174
|
+
if lane_id == "coderabbit-cli":
|
|
175
|
+
return root / "coderabbit-cli.summary.json"
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def resolve_path_for_cwd(path: Path | None, cwd: Path | None) -> Path | None:
|
|
180
|
+
if path is None:
|
|
181
|
+
return None
|
|
182
|
+
if path.is_absolute():
|
|
183
|
+
return path
|
|
184
|
+
return (cwd or Path.cwd()) / path
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def text_from_timeout_stream(value: Any) -> str:
|
|
188
|
+
if value is None:
|
|
189
|
+
return ""
|
|
190
|
+
if isinstance(value, bytes):
|
|
191
|
+
return value.decode("utf-8", errors="replace")
|
|
192
|
+
return str(value)
|