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.
Files changed (185) hide show
  1. code_mower/__init__.py +3 -0
  2. code_mower/adapters/__init__.py +39 -0
  3. code_mower/adapters/_base.py +148 -0
  4. code_mower/adapters/cursor_bugbot.py +97 -0
  5. code_mower/adapters/gitar.py +111 -0
  6. code_mower/adapters/greptile.py +191 -0
  7. code_mower/adapters/qodo.py +140 -0
  8. code_mower/antigravity_cli_audit_pr.py +241 -0
  9. code_mower/audit_handoff_log.py +345 -0
  10. code_mower/audit_labeler_lib.py +311 -0
  11. code_mower/audit_progress.py +203 -0
  12. code_mower/blind_review_artifacts.py +562 -0
  13. code_mower/blind_review_coordinator.py +276 -0
  14. code_mower/bootstrap.py +524 -0
  15. code_mower/builder_experiment.py +539 -0
  16. code_mower/calibration/__init__.py +181 -0
  17. code_mower/calibration/arms.py +267 -0
  18. code_mower/calibration/auto_discovery.py +348 -0
  19. code_mower/calibration/commands.py +192 -0
  20. code_mower/calibration/context_inputs.py +199 -0
  21. code_mower/calibration/corpus.py +91 -0
  22. code_mower/calibration/evidence.py +20 -0
  23. code_mower/calibration/evidence_report.py +360 -0
  24. code_mower/calibration/identity.py +27 -0
  25. code_mower/calibration/metrics.py +18 -0
  26. code_mower/calibration/overlap.py +103 -0
  27. code_mower/calibration/planning.py +309 -0
  28. code_mower/calibration/policy.py +207 -0
  29. code_mower/calibration/results.py +315 -0
  30. code_mower/calibration/run_results.py +147 -0
  31. code_mower/calibration/run_status.py +64 -0
  32. code_mower/calibration/runner.py +360 -0
  33. code_mower/calibration/truth.py +188 -0
  34. code_mower/calibration/value_report.py +142 -0
  35. code_mower/checks.py +402 -0
  36. code_mower/claude_audit_pr.py +1126 -0
  37. code_mower/claude_cli_bounce.py +303 -0
  38. code_mower/claude_cli_environment.py +73 -0
  39. code_mower/clear_stale.py +374 -0
  40. code_mower/cli.py +537 -0
  41. code_mower/cloud.py +674 -0
  42. code_mower/cloud_client/__init__.py +173 -0
  43. code_mower/cloud_client/bundle.py +155 -0
  44. code_mower/cloud_client/doctor.py +206 -0
  45. code_mower/cloud_client/dogfood.py +78 -0
  46. code_mower/cloud_client/endpoints.py +114 -0
  47. code_mower/cloud_client/errors.py +7 -0
  48. code_mower/cloud_client/events.py +278 -0
  49. code_mower/cloud_client/export.py +272 -0
  50. code_mower/cloud_client/git_metadata.py +46 -0
  51. code_mower/cloud_client/manifest.py +39 -0
  52. code_mower/cloud_client/operations.py +448 -0
  53. code_mower/cloud_client/reports.py +45 -0
  54. code_mower/cloud_client/setup.py +205 -0
  55. code_mower/cloud_client/upload.py +97 -0
  56. code_mower/code_mower_calibration.py +598 -0
  57. code_mower/code_mower_context_packs.py +591 -0
  58. code_mower/code_mower_merge.py +227 -0
  59. code_mower/code_mower_telemetry.py +561 -0
  60. code_mower/coderabbit_cli_audit_pr.py +526 -0
  61. code_mower/codex_audit_env_preflight.py +220 -0
  62. code_mower/codex_audit_pr.py +1738 -0
  63. code_mower/codex_audit_schema_smoke.py +160 -0
  64. code_mower/codex_audit_verdict.schema.json +44 -0
  65. code_mower/config.py +655 -0
  66. code_mower/doctor.py +161 -0
  67. code_mower/doctor_checks/__init__.py +104 -0
  68. code_mower/doctor_checks/cloud.py +129 -0
  69. code_mower/doctor_checks/common.py +215 -0
  70. code_mower/doctor_checks/github.py +128 -0
  71. code_mower/doctor_checks/github_actions.py +11 -0
  72. code_mower/doctor_checks/github_actions_cost.py +99 -0
  73. code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
  74. code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
  75. code_mower/doctor_checks/github_actions_failure_models.py +47 -0
  76. code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
  77. code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
  78. code_mower/doctor_checks/github_actions_failures.py +103 -0
  79. code_mower/doctor_checks/github_actions_permissions.py +55 -0
  80. code_mower/doctor_checks/github_api.py +79 -0
  81. code_mower/doctor_checks/github_branch.py +56 -0
  82. code_mower/doctor_checks/github_config.py +25 -0
  83. code_mower/doctor_checks/github_provider.py +61 -0
  84. code_mower/doctor_checks/github_repo.py +120 -0
  85. code_mower/doctor_checks/groups.py +36 -0
  86. code_mower/doctor_checks/models.py +96 -0
  87. code_mower/doctor_checks/output.py +86 -0
  88. code_mower/doctor_checks/presets.py +64 -0
  89. code_mower/doctor_checks/privacy.py +20 -0
  90. code_mower/doctor_checks/provider_api_model.py +138 -0
  91. code_mower/doctor_checks/provider_api_model_openai.py +29 -0
  92. code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
  93. code_mower/doctor_checks/provider_env.py +113 -0
  94. code_mower/doctor_checks/provider_env_required.py +56 -0
  95. code_mower/doctor_checks/provider_env_tokens.py +100 -0
  96. code_mower/doctor_checks/provider_local_cli.py +162 -0
  97. code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
  98. code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
  99. code_mower/doctor_checks/provider_probe.py +20 -0
  100. code_mower/doctor_checks/provider_probe_auth.py +52 -0
  101. code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
  102. code_mower/doctor_checks/provider_probe_json.py +45 -0
  103. code_mower/doctor_checks/provider_probe_remediation.py +39 -0
  104. code_mower/doctor_checks/providers.py +159 -0
  105. code_mower/doctor_checks/registry.py +69 -0
  106. code_mower/doctor_checks/runner.py +188 -0
  107. code_mower/doctor_checks/runtime.py +89 -0
  108. code_mower/doctor_checks/runtime_github_auth.py +148 -0
  109. code_mower/gemini_cli_audit_pr.py +897 -0
  110. code_mower/hermes_cli_audit_pr.py +436 -0
  111. code_mower/init.py +888 -0
  112. code_mower/lane_configs/__init__.py +37 -0
  113. code_mower/lane_configs/aider.py +32 -0
  114. code_mower/lane_configs/antigravity_cli.py +35 -0
  115. code_mower/lane_configs/claude.py +35 -0
  116. code_mower/lane_configs/codex.py +32 -0
  117. code_mower/lane_configs/devin.py +33 -0
  118. code_mower/lane_configs/gemini_cli.py +35 -0
  119. code_mower/lane_configs/hermes_cli.py +35 -0
  120. code_mower/lane_configs/local_llm.py +31 -0
  121. code_mower/local_llm_audit_pr.py +1364 -0
  122. code_mower/local_llm_bakeoff.py +458 -0
  123. code_mower/local_llm_calibration.py +441 -0
  124. code_mower/local_llm_profiles.py +66 -0
  125. code_mower/migration.py +508 -0
  126. code_mower/migration_install.py +292 -0
  127. code_mower/migration_mirror.py +392 -0
  128. code_mower/migration_readiness.py +237 -0
  129. code_mower/migration_rehearsal.py +718 -0
  130. code_mower/next_steps.py +441 -0
  131. code_mower/package.py +673 -0
  132. code_mower/package_content.py +444 -0
  133. code_mower/package_manifest.py +452 -0
  134. code_mower/package_paths.py +53 -0
  135. code_mower/package_rendering.py +90 -0
  136. code_mower/package_static.py +585 -0
  137. code_mower/prompts.py +267 -0
  138. code_mower/provider_registry.py +469 -0
  139. code_mower/provider_runners/__init__.py +60 -0
  140. code_mower/provider_runners/comments.py +31 -0
  141. code_mower/provider_runners/git.py +46 -0
  142. code_mower/provider_runners/github_auth.py +61 -0
  143. code_mower/provider_runners/github_pr.py +120 -0
  144. code_mower/provider_runners/process.py +58 -0
  145. code_mower/provider_runners/repo_paths.py +23 -0
  146. code_mower/provider_runners/text_schema.py +41 -0
  147. code_mower/provider_runners/verdict_artifacts.py +103 -0
  148. code_mower/provider_runners/workspace.py +57 -0
  149. code_mower/release_readiness.py +549 -0
  150. code_mower/reviewer_metrics.py +389 -0
  151. code_mower/saas_reviewer_labeler.py +809 -0
  152. code_mower/secrets.py +89 -0
  153. code_mower/templates/builder-experiment.example.json +55 -0
  154. code_mower/templates/calibration-corpus.example.json +129 -0
  155. code_mower/templates/calibration-corpus.json +129 -0
  156. code_mower/templates/code-mower.example.yml +423 -0
  157. code_mower/templates/context-packs.example.json +150 -0
  158. code_mower/templates/lane_prompts/base-audit.md +22 -0
  159. code_mower/templates/lane_prompts/calibration-policy.md +21 -0
  160. code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
  161. code_mower/templates/lane_prompts/docs-design.md +12 -0
  162. code_mower/templates/lane_prompts/generic-programming.md +21 -0
  163. code_mower/templates/lane_prompts/operability.md +22 -0
  164. code_mower/templates/lane_prompts/package-runtime.md +12 -0
  165. code_mower/templates/lane_prompts/security-threat-model.md +22 -0
  166. code_mower/templates/product-support/code_mower +216 -0
  167. code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
  168. code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
  169. code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
  170. code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
  171. code_mower/templates/product-support/safe_gh_comment.py +96 -0
  172. code_mower/templates/providers.yml +454 -0
  173. code_mower/templates/reviewer-spend.example.json +28 -0
  174. code_mower/templates/reviewer-value-report.example.md +20 -0
  175. code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
  176. code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
  177. code_mower/trailer_comment_labeler.py +207 -0
  178. code_mower/versioning.py +32 -0
  179. code_mower-0.5.0b5.dist-info/METADATA +302 -0
  180. code_mower-0.5.0b5.dist-info/RECORD +185 -0
  181. code_mower-0.5.0b5.dist-info/WHEEL +5 -0
  182. code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
  183. code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
  184. code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
  185. code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,27 @@
1
+ """Stable identifiers for calibration artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import re
7
+ from typing import Any
8
+
9
+
10
+ SAFE_SLUG_RE = re.compile(r"[^A-Za-z0-9_.-]+")
11
+
12
+
13
+ def safe_slug(value: Any, fallback: str = "item") -> str:
14
+ text = SAFE_SLUG_RE.sub("-", str(value or "").strip()).strip("._-")
15
+ while ".." in text:
16
+ text = text.replace("..", ".")
17
+ text = text.strip("._-")
18
+ return text or fallback
19
+
20
+
21
+ def head_slug(value: Any) -> str:
22
+ head = str(value or "").strip()
23
+ if not head:
24
+ return ""
25
+ safe_head = safe_slug(head, "")
26
+ digest = hashlib.sha256(head.encode("utf-8")).hexdigest()[:12]
27
+ return f"{safe_head[:12] or digest}-{digest}"
@@ -0,0 +1,18 @@
1
+ """Calibration metric normalization helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def float_or_zero(value: Any) -> float:
9
+ if isinstance(value, bool):
10
+ return 0.0
11
+ if isinstance(value, (int, float)):
12
+ return float(value)
13
+ if isinstance(value, str):
14
+ try:
15
+ return float(value)
16
+ except ValueError:
17
+ return 0.0
18
+ return 0.0
@@ -0,0 +1,103 @@
1
+ """Calibration finding-overlap reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from itertools import combinations
7
+ from typing import Any, Iterable, Mapping
8
+
9
+
10
+ def finding_key(finding: Mapping[str, Any]) -> str:
11
+ raw = "\n".join(
12
+ [
13
+ str(finding.get("repo") or ""),
14
+ str(finding.get("pr_number") or ""),
15
+ str(finding.get("head_sha") or ""),
16
+ str(finding.get("severity") or ""),
17
+ str(finding.get("path") or ""),
18
+ " ".join(str(finding.get("text") or "").lower().split()),
19
+ ]
20
+ )
21
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
22
+
23
+
24
+ def build_overlap_report(reports: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
25
+ profile_findings: dict[str, set[str]] = {}
26
+ profile_sources: dict[str, set[str]] = {}
27
+ report_count = 0
28
+
29
+ for report in reports:
30
+ report_count += 1
31
+ if report.get("mode") != "local-llm-calibration":
32
+ raise ValueError("overlap reports currently expect local-llm-calibration inputs")
33
+ source = ",".join(str(item) for item in report.get("sources", []) or [])
34
+ profiles = report.get("profiles")
35
+ if isinstance(profiles, Mapping):
36
+ for profile_id in profiles:
37
+ profile_id = str(profile_id)
38
+ if not profile_id:
39
+ continue
40
+ profile_findings.setdefault(profile_id, set())
41
+ if source:
42
+ profile_sources.setdefault(profile_id, set()).add(source)
43
+ for finding in report.get("findings", []) or []:
44
+ if not isinstance(finding, Mapping):
45
+ continue
46
+ profile_id = str(finding.get("profile_id") or "")
47
+ if not profile_id:
48
+ continue
49
+ profile_findings.setdefault(profile_id, set()).add(finding_key(finding))
50
+ if source:
51
+ profile_sources.setdefault(profile_id, set()).add(source)
52
+
53
+ pairs: list[dict[str, Any]] = []
54
+ for left, right in combinations(sorted(profile_findings), 2):
55
+ left_set = profile_findings[left]
56
+ right_set = profile_findings[right]
57
+ union = left_set | right_set
58
+ intersection = left_set & right_set
59
+ jaccard_similarity = len(intersection) / len(union) if union else 1.0
60
+ pairs.append(
61
+ {
62
+ "left": left,
63
+ "right": right,
64
+ "left_findings": len(left_set),
65
+ "right_findings": len(right_set),
66
+ "shared_findings": len(intersection),
67
+ "union_findings": len(union),
68
+ "jaccard_similarity": round(jaccard_similarity, 4),
69
+ "jaccard_distance": round(1.0 - jaccard_similarity, 4),
70
+ }
71
+ )
72
+
73
+ return {
74
+ "mode": "code-mower-calibration-overlap",
75
+ "source_report_count": report_count,
76
+ "profiles": {
77
+ profile_id: {
78
+ "finding_count": len(findings),
79
+ "sources": sorted(profile_sources.get(profile_id, set())),
80
+ }
81
+ for profile_id, findings in sorted(profile_findings.items())
82
+ },
83
+ "pairs": pairs,
84
+ "caveat": (
85
+ "Exact text overlap is a conservative proxy. Human adjudication or "
86
+ "semantic clustering is still required before making merge-gate decisions."
87
+ ),
88
+ }
89
+
90
+
91
+ def render_overlap_text(report: Mapping[str, Any]) -> str:
92
+ lines = ["Code Mower calibration overlap", "", "Pairs:"]
93
+ for pair in report.get("pairs", []) or []:
94
+ if isinstance(pair, Mapping):
95
+ lines.append(
96
+ f"- {pair.get('left')} vs {pair.get('right')}: "
97
+ f"shared={pair.get('shared_findings')} "
98
+ f"distance={pair.get('jaccard_distance')}"
99
+ )
100
+ if not report.get("pairs"):
101
+ lines.append("- none")
102
+ lines.extend(["", f"Caveat: {report.get('caveat', '')}"])
103
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,309 @@
1
+ """Calibration pilot planning and command templates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Mapping, Sequence
7
+
8
+ from .arms import default_arms
9
+ from .identity import head_slug, safe_slug
10
+
11
+
12
+ def _run_output_dir(output_dir: Path, item: Mapping[str, Any], arm_id: str, replicate: int) -> Path:
13
+ repo_slug = safe_slug(str(item["repo"]).replace("/", "__"), "repo")
14
+ item_head_slug = head_slug(item.get("head_sha", ""))
15
+ pr_dir = f"pr-{item['pr_number']}" + (f"-{item_head_slug}" if item_head_slug else "")
16
+ return output_dir / repo_slug / pr_dir / f"r{replicate}" / arm_id
17
+
18
+
19
+ def _run_id(item: Mapping[str, Any], *, arm_id: str, replicate: int) -> str:
20
+ base = f"{safe_slug(str(item['repo']).replace('/', '__'))}-pr-{item['pr_number']}"
21
+ item_head_slug = head_slug(item.get("head_sha", ""))
22
+ if item_head_slug:
23
+ base = f"{base}-{item_head_slug}"
24
+ return f"{base}-r{replicate}-{arm_id}"
25
+
26
+
27
+ def _local_llm_command(
28
+ item: Mapping[str, Any],
29
+ *,
30
+ profiles: Sequence[str],
31
+ output_dir: Path,
32
+ jobs: int,
33
+ repo_path: str = "/path/to/pr-worktree",
34
+ ) -> list[str]:
35
+ command = [
36
+ "code-mower",
37
+ "local-llm",
38
+ "bakeoff",
39
+ "--repo",
40
+ str(item["repo"]),
41
+ "--pr",
42
+ str(item["pr_number"]),
43
+ "--profiles",
44
+ ",".join(profiles),
45
+ "--output-dir",
46
+ str(output_dir / "local-llm"),
47
+ "--jobs",
48
+ str(jobs),
49
+ "--json",
50
+ ]
51
+ if item.get("head_sha"):
52
+ command.extend(["--expected-head-sha", str(item["head_sha"])])
53
+ if item.get("base_ref"):
54
+ command.extend(["--repo-path", repo_path])
55
+ command.extend(["--base-ref", str(item["base_ref"])])
56
+ return command
57
+
58
+
59
+ def _local_cli_command(
60
+ item: Mapping[str, Any],
61
+ *,
62
+ lane: str,
63
+ output_dir: Path,
64
+ prompt_lenses: Sequence[str] = ("base-audit",),
65
+ output_leaf: str,
66
+ repo_path: str = "/path/to/pr-worktree",
67
+ ) -> list[str]:
68
+ command = [
69
+ "code-mower",
70
+ lane,
71
+ "--repo",
72
+ str(item["repo"]),
73
+ "--pr",
74
+ str(item["pr_number"]),
75
+ "--prompt-lenses",
76
+ ",".join(prompt_lenses),
77
+ "--output-dir",
78
+ str(output_dir / output_leaf),
79
+ "--json",
80
+ ]
81
+ if item.get("head_sha"):
82
+ command.extend(["--expected-head-sha", str(item["head_sha"])])
83
+ if item.get("base_ref"):
84
+ command.extend(["--repo-path", repo_path])
85
+ command.extend(["--base-ref", str(item["base_ref"])])
86
+ return command
87
+
88
+
89
+ def _coderabbit_cli_command(
90
+ item: Mapping[str, Any],
91
+ *,
92
+ output_dir: Path,
93
+ repo_path: str = "/path/to/pr-worktree",
94
+ base_ref: str = "origin/main",
95
+ ) -> list[str]:
96
+ base_ref_value = str(item.get("base_ref") or base_ref)
97
+ command = [
98
+ "code-mower",
99
+ "coderabbit-cli",
100
+ "--repo",
101
+ str(item["repo"]),
102
+ "--pr",
103
+ str(item["pr_number"]),
104
+ "--repo-path",
105
+ repo_path,
106
+ "--base-ref",
107
+ base_ref_value,
108
+ "--output-dir",
109
+ str(output_dir / "coderabbit-cli"),
110
+ "--json",
111
+ ]
112
+ if item.get("head_sha"):
113
+ command.extend(["--expected-head-sha", str(item["head_sha"])])
114
+ return command
115
+
116
+
117
+ def _local_cli_output_leaf(*, lane_slug: str, lane_id: str, reviewer_id: str) -> str:
118
+ if reviewer_id in {lane_slug, lane_id, lane_slug.replace("-", "_")}:
119
+ return lane_slug
120
+ return safe_slug(reviewer_id, lane_slug)
121
+
122
+
123
+ def render_plan_text(plan: Mapping[str, Any]) -> str:
124
+ lines = [
125
+ "Code Mower calibration pilot plan",
126
+ f"Corpus: {plan.get('corpus_name', '')}",
127
+ f"Runs: {plan.get('run_count', 0)}",
128
+ "",
129
+ "Arms:",
130
+ ]
131
+ for arm in plan.get("arms", []) or []:
132
+ if isinstance(arm, Mapping):
133
+ lines.append(f"- {arm.get('arm_id')}: {arm.get('kind')} - {arm.get('purpose')}")
134
+ lines.extend(["", "First commands:"])
135
+ shown = 0
136
+ for run in plan.get("runs", []) or []:
137
+ if not isinstance(run, Mapping):
138
+ continue
139
+ if run.get("requires_explicit_arm"):
140
+ continue
141
+ for command in run.get("commands", []) or []:
142
+ if shown >= 3:
143
+ break
144
+ lines.append("- " + " ".join(str(part) for part in command))
145
+ shown += 1
146
+ if shown >= 3:
147
+ break
148
+ if shown == 0:
149
+ lines.append("- none; this corpus currently needs manual structured audit invocations")
150
+ return "\n".join(lines) + "\n"
151
+
152
+
153
+ def build_pilot_plan(
154
+ corpus: Mapping[str, Any],
155
+ *,
156
+ replicates: int = 1,
157
+ output_dir: Path = Path(".code-mower/calibration"),
158
+ jobs: int = 1,
159
+ ) -> dict[str, Any]:
160
+ if replicates <= 0:
161
+ raise ValueError("replicates must be greater than zero")
162
+ if jobs <= 0:
163
+ raise ValueError("jobs must be greater than zero")
164
+
165
+ arms = default_arms()
166
+ runs: list[dict[str, Any]] = []
167
+ for item in corpus["corpus"]:
168
+ for replicate in range(1, replicates + 1):
169
+ for arm in arms:
170
+ arm_id = str(arm["arm_id"])
171
+ run_dir = _run_output_dir(output_dir, item, arm_id, replicate)
172
+ run: dict[str, Any] = {
173
+ "run_id": _run_id(item, arm_id=arm_id, replicate=replicate),
174
+ "repo": item["repo"],
175
+ "pr_number": item["pr_number"],
176
+ "head_sha": item.get("head_sha", ""),
177
+ "base_ref": item.get("base_ref", ""),
178
+ "review_class": item.get("review_class", "general"),
179
+ "context_packs": item.get("context_packs", []),
180
+ "replicate": replicate,
181
+ "arm_id": arm_id,
182
+ "kind": arm["kind"],
183
+ "reviewers": arm["reviewers"],
184
+ "output_dir": str(run_dir),
185
+ "blind_review_required": True,
186
+ "requires_explicit_arm": bool(arm.get("requires_explicit_arm")),
187
+ }
188
+ local_profiles = [
189
+ reviewer["profile_id"]
190
+ for reviewer in arm["reviewers"]
191
+ if reviewer.get("provider") == "local-llm"
192
+ ]
193
+ if local_profiles:
194
+ commands = run.setdefault("commands", [])
195
+ command_metadata = run.setdefault("command_metadata", [])
196
+ commands.append(
197
+ _local_llm_command(
198
+ item,
199
+ profiles=local_profiles,
200
+ output_dir=run_dir,
201
+ jobs=min(jobs, len(local_profiles)),
202
+ )
203
+ )
204
+ command_metadata.append(
205
+ {
206
+ "lane_id": "local-llm",
207
+ "reviewer_ids": list(local_profiles),
208
+ }
209
+ )
210
+ cli_reviewers = [
211
+ reviewer
212
+ for reviewer in arm["reviewers"]
213
+ if reviewer.get("provider") == "local-cli"
214
+ ]
215
+ if cli_reviewers:
216
+ commands = run.setdefault("commands", [])
217
+ command_metadata = run.setdefault("command_metadata", [])
218
+ notes = run.setdefault("notes", [])
219
+ for reviewer in cli_reviewers:
220
+ lane_id = str(reviewer["lane_id"])
221
+ lane_slug = lane_id.replace("_", "-")
222
+ reviewer_id = str(reviewer.get("reviewer_id") or lane_slug)
223
+ if reviewer_id == lane_id:
224
+ reviewer_id = lane_slug
225
+ lenses = [
226
+ str(lens)
227
+ for lens in reviewer.get("lenses", ["base-audit"])
228
+ if str(lens).strip()
229
+ ] or ["base-audit"]
230
+ if lane_id in {"antigravity_cli", "gemini_cli", "hermes_cli"}:
231
+ output_leaf = _local_cli_output_leaf(
232
+ lane_slug=lane_slug,
233
+ lane_id=lane_id,
234
+ reviewer_id=reviewer_id,
235
+ )
236
+ commands.append(
237
+ _local_cli_command(
238
+ item,
239
+ lane=lane_slug,
240
+ output_dir=run_dir,
241
+ prompt_lenses=lenses,
242
+ output_leaf=output_leaf,
243
+ )
244
+ )
245
+ command_metadata.append(
246
+ {
247
+ "lane_id": lane_slug,
248
+ "reviewer_id": reviewer_id,
249
+ "lenses": lenses,
250
+ }
251
+ )
252
+ elif lane_id == "coderabbit_cli":
253
+ commands.append(
254
+ _coderabbit_cli_command(
255
+ item,
256
+ output_dir=run_dir,
257
+ )
258
+ )
259
+ command_metadata.append(
260
+ {
261
+ "lane_id": lane_slug,
262
+ "reviewer_id": reviewer_id,
263
+ "lenses": lenses,
264
+ }
265
+ )
266
+ run.setdefault("manual_steps", []).append(
267
+ (
268
+ "Check out the PR head in a clean local worktree, then "
269
+ "replace /path/to/pr-worktree in the coderabbit-cli command."
270
+ )
271
+ )
272
+ else:
273
+ run.setdefault("manual_steps", []).append(
274
+ (
275
+ f"Run {reviewer_id} against {item['repo']}#{item['pr_number']} "
276
+ "under the same head SHA, then store its JSON summary beside this run."
277
+ )
278
+ )
279
+ notes.append(
280
+ f"{reviewer_id} is informational calibration evidence, not merge authority."
281
+ )
282
+ if not run.get("commands") and not run.get("manual_steps"):
283
+ run["manual_steps"] = [
284
+ "Invoke each listed reviewer through its structured audit lane and keep outputs hidden until every reviewer in this arm finishes."
285
+ ]
286
+ if run.get("commands") and len(run.get("commands", [])) != len(
287
+ run.get("command_metadata", [])
288
+ ):
289
+ raise ValueError(
290
+ f"command/metadata desync in run {run.get('run_id')}"
291
+ )
292
+ runs.append(run)
293
+
294
+ return {
295
+ "mode": "code-mower-calibration-pilot-plan",
296
+ "corpus_name": corpus.get("name", ""),
297
+ "description": corpus.get("description", ""),
298
+ "replicates": replicates,
299
+ "output_dir": str(output_dir),
300
+ "arms": arms,
301
+ "run_count": len(runs),
302
+ "runs": runs,
303
+ "guardrails": [
304
+ "Run all reviewers on the same head SHA for a corpus item.",
305
+ "Keep arm outputs hidden until every reviewer in that arm completes.",
306
+ "Use this as calibration evidence, not merge authority.",
307
+ "Adjudicate findings after collection before comparing accuracy.",
308
+ ],
309
+ }
@@ -0,0 +1,207 @@
1
+ """Calibration lane-promotion policy thresholds and report helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .metrics import float_or_zero as _float
9
+
10
+ MERGE_GATE_USEFUL_RATE = 0.60
11
+ SELECTIVE_USEFUL_RATE = 0.50
12
+ MERGE_GATE_MIN_FINDINGS = 10
13
+ MERGE_GATE_MIN_CLEAN_RUNS = 2
14
+
15
+
16
+ def build_lane_policy_report(metrics: Mapping[str, Any]) -> dict[str, Any]:
17
+ """Build lane-promotion policy recommendations from reviewer metrics."""
18
+ if metrics.get("mode") != "reviewer-metrics":
19
+ raise ValueError("lane policy expects a reviewer-metrics report")
20
+ profiles = metrics.get("profiles", {})
21
+ if not isinstance(profiles, Mapping):
22
+ raise ValueError("reviewer-metrics report profiles must be a mapping")
23
+
24
+ policies: dict[str, dict[str, Any]] = {}
25
+ for profile_id, stats in sorted(profiles.items()):
26
+ if not isinstance(stats, Mapping):
27
+ continue
28
+ useful_rate = _float(stats.get("useful_rate"))
29
+ useful_findings = int(stats.get("useful_findings") or 0)
30
+ known_findings = int(stats.get("known_disposition_count") or 0)
31
+ clean_pass_runs = int(stats.get("known_clean_pass_runs") or 0)
32
+ false_positive_runs = int(stats.get("blocking_false_positive_runs") or 0)
33
+ known_blocked_missed_runs = int(stats.get("known_blocked_missed_runs") or 0)
34
+ infra_error_runs = int(stats.get("infra_error_runs") or 0)
35
+ audit_input_insufficient_runs = int(
36
+ stats.get("audit_input_insufficient_runs") or 0
37
+ )
38
+ review_classes = [
39
+ str(item)
40
+ for item in stats.get("review_classes", []) or []
41
+ if str(item).strip()
42
+ ]
43
+ useful_review_classes = [
44
+ str(item)
45
+ for item in stats.get("useful_review_classes", []) or []
46
+ if str(item).strip()
47
+ ]
48
+ context_packs = [
49
+ str(item)
50
+ for item in stats.get("context_packs", []) or []
51
+ if str(item).strip()
52
+ ]
53
+ useful_context_packs = [
54
+ str(item)
55
+ for item in stats.get("useful_context_packs", []) or []
56
+ if str(item).strip()
57
+ ]
58
+ narrow_useful_review_classes = [
59
+ review_class
60
+ for review_class in useful_review_classes
61
+ if review_class not in {"", "general"}
62
+ ]
63
+ event_log = stats.get("event_log", {})
64
+ observed_pr_count = (
65
+ int(event_log.get("observed_pr_count") or 0)
66
+ if isinstance(event_log, Mapping)
67
+ else 0
68
+ )
69
+ reasons: list[str] = []
70
+ if known_findings < MERGE_GATE_MIN_FINDINGS:
71
+ reasons.append(
72
+ f"needs at least {MERGE_GATE_MIN_FINDINGS} adjudicated findings"
73
+ )
74
+ if clean_pass_runs < MERGE_GATE_MIN_CLEAN_RUNS:
75
+ reasons.append(
76
+ f"needs at least {MERGE_GATE_MIN_CLEAN_RUNS} known-clean zero-blocker runs"
77
+ )
78
+ if false_positive_runs:
79
+ reasons.append("has known-clean blocking false positives")
80
+ if known_blocked_missed_runs:
81
+ reasons.append("missed known-blocked calibration runs")
82
+ if infra_error_runs:
83
+ reasons.append("has infra/setup failures to stabilize before promotion")
84
+ if audit_input_insufficient_runs:
85
+ reasons.append("needs richer audit input/context before promotion")
86
+ if useful_rate < SELECTIVE_USEFUL_RATE:
87
+ reasons.append("useful-rate below selective-trigger threshold")
88
+ if (
89
+ known_findings >= MERGE_GATE_MIN_FINDINGS
90
+ and clean_pass_runs >= MERGE_GATE_MIN_CLEAN_RUNS
91
+ and false_positive_runs == 0
92
+ and known_blocked_missed_runs == 0
93
+ and infra_error_runs == 0
94
+ and audit_input_insufficient_runs == 0
95
+ and useful_rate >= MERGE_GATE_USEFUL_RATE
96
+ ):
97
+ classification = "merge_gate_candidate"
98
+ elif (
99
+ useful_findings > 0
100
+ and useful_rate >= SELECTIVE_USEFUL_RATE
101
+ and bool(narrow_useful_review_classes)
102
+ and false_positive_runs == 0
103
+ and clean_pass_runs >= MERGE_GATE_MIN_CLEAN_RUNS
104
+ and known_blocked_missed_runs == 0
105
+ and infra_error_runs == 0
106
+ and audit_input_insufficient_runs == 0
107
+ ):
108
+ classification = "selective_trigger_candidate"
109
+ else:
110
+ classification = "informational"
111
+
112
+ if (
113
+ classification == "informational"
114
+ and useful_findings > 0
115
+ and useful_rate >= SELECTIVE_USEFUL_RATE
116
+ and not narrow_useful_review_classes
117
+ ):
118
+ reasons.append(
119
+ "needs useful non-general review-class evidence for selective triggers"
120
+ )
121
+
122
+ if classification == "merge_gate_candidate":
123
+ recommended_role = "merge_gate_eligible"
124
+ automatic_trigger = "repo_merge_bar_opt_in"
125
+ elif classification == "selective_trigger_candidate":
126
+ recommended_role = "selective_trigger"
127
+ automatic_trigger = "matching_review_class_only"
128
+ else:
129
+ recommended_role = "informational"
130
+ automatic_trigger = "manual_or_calibration_only"
131
+
132
+ suggested_trigger_classes = (
133
+ narrow_useful_review_classes
134
+ if classification == "selective_trigger_candidate"
135
+ else []
136
+ )
137
+
138
+ policies[str(profile_id)] = {
139
+ "profile_id": str(profile_id),
140
+ "classification": classification,
141
+ "recommended_role": recommended_role,
142
+ "automatic_trigger": automatic_trigger,
143
+ "suggested_trigger_classes": suggested_trigger_classes,
144
+ "review_classes": review_classes,
145
+ "context_packs": context_packs,
146
+ "useful_review_classes": useful_review_classes,
147
+ "useful_context_packs": useful_context_packs,
148
+ "useful_rate": (
149
+ useful_rate if stats.get("useful_rate") is not None else None
150
+ ),
151
+ "useful_findings": useful_findings,
152
+ "known_disposition_count": known_findings,
153
+ "known_clean_pass_runs": clean_pass_runs,
154
+ "blocking_false_positive_runs": false_positive_runs,
155
+ "known_blocked_runs": int(stats.get("known_blocked_runs") or 0),
156
+ "known_blocked_caught_runs": int(
157
+ stats.get("known_blocked_caught_runs") or 0
158
+ ),
159
+ "known_blocked_missed_runs": known_blocked_missed_runs,
160
+ "infra_error_runs": infra_error_runs,
161
+ "audit_input_insufficient_runs": audit_input_insufficient_runs,
162
+ "observed_pr_count": observed_pr_count,
163
+ "reasons": reasons
164
+ or [
165
+ "evidence meets current threshold heuristics; "
166
+ "keep human review in the loop"
167
+ ],
168
+ }
169
+
170
+ return {
171
+ "mode": "code-mower-lane-policy",
172
+ "source_mode": metrics.get("mode"),
173
+ "thresholds": {
174
+ "merge_gate_useful_rate": MERGE_GATE_USEFUL_RATE,
175
+ "selective_useful_rate": SELECTIVE_USEFUL_RATE,
176
+ "merge_gate_min_findings": MERGE_GATE_MIN_FINDINGS,
177
+ "merge_gate_min_clean_runs": MERGE_GATE_MIN_CLEAN_RUNS,
178
+ },
179
+ "policies": policies,
180
+ "caveat": (
181
+ "This is a policy recommendation from calibration evidence, not an "
182
+ "automatic repository merge-rule change."
183
+ ),
184
+ }
185
+
186
+
187
+ def render_policy_text(report: Mapping[str, Any]) -> str:
188
+ lines = ["Code Mower lane policy", "", "Policies:"]
189
+ policies = report.get("policies", {})
190
+ if isinstance(policies, Mapping) and policies:
191
+ for profile_id, policy in policies.items():
192
+ if not isinstance(policy, Mapping):
193
+ continue
194
+ lines.append(
195
+ f"- {profile_id}: {policy.get('classification')} "
196
+ f"role={policy.get('recommended_role')} "
197
+ f"trigger={policy.get('automatic_trigger')} "
198
+ f"useful_rate={policy.get('useful_rate')} "
199
+ f"clean_passes={policy.get('known_clean_pass_runs', 0)}"
200
+ )
201
+ reasons = policy.get("reasons", [])
202
+ if isinstance(reasons, list) and reasons:
203
+ lines.append(f" reasons: {'; '.join(str(reason) for reason in reasons)}")
204
+ else:
205
+ lines.append("- none")
206
+ lines.extend(["", f"Caveat: {report.get('caveat', '')}"])
207
+ return "\n".join(lines) + "\n"