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,360 @@
1
+ """Calibration command execution orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as _dt
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import Any, Mapping, Sequence
13
+
14
+ from .commands import (
15
+ command_lane_id,
16
+ command_metadata_for_run,
17
+ local_llm_profiles_from_command,
18
+ materialize_command,
19
+ resolve_path_for_cwd,
20
+ reviewer_id_from_command,
21
+ summary_path_for_command,
22
+ text_from_timeout_stream,
23
+ )
24
+ from .context_inputs import context_pack_file_for_command
25
+ from .corpus import load_json_object
26
+ from .identity import safe_slug
27
+ from .planning import build_pilot_plan
28
+ from .results import infra_run_record, run_records_from_summary
29
+ from .run_results import CALIBRATION_RUN_RESULTS_MODE, CALIBRATION_RUN_RESULTS_SCHEMA
30
+
31
+
32
+ def utc_now_iso() -> str:
33
+ return (
34
+ _dt.datetime.now(_dt.timezone.utc)
35
+ .replace(microsecond=0)
36
+ .isoformat()
37
+ .replace("+00:00", "Z")
38
+ )
39
+
40
+
41
+ def default_code_mower_command() -> list[str]:
42
+ module_path = Path(__file__).resolve()
43
+ package_root = module_path.parent.parent
44
+ legacy_cli = package_root / "code_mower_cli.py"
45
+ if legacy_cli.exists():
46
+ return [sys.executable, str(legacy_cli)]
47
+ packaged_cli = package_root / "cli.py"
48
+ if packaged_cli.exists(): # pragma: no cover - exercised after package extraction.
49
+ return [sys.executable, "-m", "code_mower.cli"]
50
+ return ["code-mower"]
51
+
52
+
53
+ def load_summary(path: Path | None) -> Mapping[str, Any] | None:
54
+ if path is None or not path.is_file():
55
+ return None
56
+ return load_json_object(path)
57
+
58
+
59
+ def write_json(path: Path, payload: Mapping[str, Any]) -> None:
60
+ path.parent.mkdir(parents=True, exist_ok=True)
61
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
62
+
63
+
64
+ def result_command_dir(results_dir: Path, run_id: str, command_index: int, lane_id: str) -> Path:
65
+ return results_dir / safe_slug(run_id, "run") / f"{command_index:02d}-{safe_slug(lane_id, 'lane')}"
66
+
67
+
68
+ def run_calibration_commands(
69
+ corpus: Mapping[str, Any],
70
+ *,
71
+ replicates: int = 1,
72
+ output_dir: Path = Path(".code-mower/calibration"),
73
+ results_dir: Path = Path(".code-mower/calibration-results"),
74
+ lanes: Sequence[str] = (),
75
+ arms: Sequence[str] = (),
76
+ jobs: int = 1,
77
+ code_mower_command: Sequence[str] | None = None,
78
+ repo_path_map: Mapping[str, str] | None = None,
79
+ context_pack_manifest: Mapping[str, Any] | None = None,
80
+ context_pack_output_dir: Path = Path(".code-mower/calibration-context-packs"),
81
+ require_context_pack_files: bool = False,
82
+ allow_historical_head: bool = False,
83
+ dry_run: bool = False,
84
+ limit: int | None = None,
85
+ timeout_seconds: int = 1800,
86
+ cwd: Path | None = None,
87
+ ) -> dict[str, Any]:
88
+ """Run selected calibration commands and persist raw output plus summaries."""
89
+
90
+ selected_lanes = {lane.replace("_", "-") for lane in lanes if lane}
91
+ selected_arms = {str(arm).strip() for arm in arms if str(arm).strip()}
92
+ code_mower_command = tuple(code_mower_command or default_code_mower_command())
93
+ repo_path_map = repo_path_map or {}
94
+ plan = build_pilot_plan(
95
+ corpus,
96
+ replicates=replicates,
97
+ output_dir=output_dir,
98
+ jobs=jobs,
99
+ )
100
+ started_at = utc_now_iso()
101
+ command_results: list[dict[str, Any]] = []
102
+ reviewer_runs: list[dict[str, Any]] = []
103
+ executed = 0
104
+ skipped = 0
105
+ prevalidated = 0
106
+
107
+ for run in plan.get("runs", []) or []:
108
+ if not isinstance(run, Mapping):
109
+ continue
110
+ arm_id = str(run.get("arm_id") or "")
111
+ if selected_arms:
112
+ if arm_id not in selected_arms:
113
+ continue
114
+ elif run.get("requires_explicit_arm"):
115
+ continue
116
+ item = {
117
+ "repo": run.get("repo"),
118
+ "pr_number": run.get("pr_number"),
119
+ "head_sha": run.get("head_sha"),
120
+ "base_ref": run.get("base_ref"),
121
+ }
122
+ for command in run.get("commands", []) or []:
123
+ if not isinstance(command, list):
124
+ continue
125
+ lane_id = command_lane_id(command)
126
+ if selected_lanes and lane_id not in selected_lanes:
127
+ continue
128
+ if limit is not None and prevalidated >= limit:
129
+ continue
130
+ materialize_command(
131
+ command,
132
+ item=item,
133
+ code_mower_command=code_mower_command,
134
+ repo_path_map=repo_path_map,
135
+ allow_historical_head=allow_historical_head,
136
+ )
137
+ prevalidated += 1
138
+
139
+ for run in plan.get("runs", []) or []:
140
+ if not isinstance(run, Mapping):
141
+ continue
142
+ arm_id = str(run.get("arm_id") or "")
143
+ if selected_arms:
144
+ if arm_id not in selected_arms:
145
+ continue
146
+ elif run.get("requires_explicit_arm"):
147
+ continue
148
+ item = {
149
+ "repo": run.get("repo"),
150
+ "pr_number": run.get("pr_number"),
151
+ "head_sha": run.get("head_sha"),
152
+ "base_ref": run.get("base_ref"),
153
+ "calibration_run_id": run.get("run_id"),
154
+ "replicate": run.get("replicate"),
155
+ }
156
+ corpus_item = next(
157
+ (
158
+ candidate
159
+ for candidate in corpus.get("corpus", []) or []
160
+ if isinstance(candidate, Mapping)
161
+ and candidate.get("repo") == run.get("repo")
162
+ and candidate.get("pr_number") == run.get("pr_number")
163
+ and candidate.get("head_sha", "") == run.get("head_sha", "")
164
+ ),
165
+ item,
166
+ )
167
+ item["context_packs"] = list(corpus_item.get("context_packs", []) or [])
168
+ for command_index, command in enumerate(run.get("commands", []) or []):
169
+ if not isinstance(command, list):
170
+ continue
171
+ lane_id = command_lane_id(command)
172
+ if selected_lanes and lane_id not in selected_lanes:
173
+ skipped += 1
174
+ continue
175
+ if limit is not None and executed >= limit:
176
+ skipped += 1
177
+ continue
178
+ command_metadata = command_metadata_for_run(run, command_index)
179
+ reviewer_id = str(
180
+ command_metadata.get("reviewer_id")
181
+ or reviewer_id_from_command(command)
182
+ )
183
+ materialized = materialize_command(
184
+ command,
185
+ item=item,
186
+ code_mower_command=code_mower_command,
187
+ repo_path_map=repo_path_map,
188
+ allow_historical_head=allow_historical_head,
189
+ )
190
+ result_dir = result_command_dir(
191
+ results_dir,
192
+ str(run.get("run_id") or "run"),
193
+ command_index,
194
+ lane_id,
195
+ )
196
+ result_dir.mkdir(parents=True, exist_ok=True)
197
+ context_pack_path = context_pack_file_for_command(
198
+ item=item,
199
+ lane_id=lane_id,
200
+ result_dir=result_dir,
201
+ repo_path_map=repo_path_map,
202
+ context_pack_manifest=context_pack_manifest,
203
+ context_pack_output_dir=context_pack_output_dir,
204
+ require_context_pack_files=require_context_pack_files,
205
+ )
206
+ if context_pack_path is not None:
207
+ materialized.extend(["--context-pack-file", str(context_pack_path)])
208
+ planned_args = list(command)
209
+ if context_pack_path is not None:
210
+ planned_args.extend(["--context-pack-file", str(context_pack_path)])
211
+ command_path = result_dir / "command.json"
212
+ stdout_path = result_dir / "stdout.txt"
213
+ stderr_path = result_dir / "stderr.txt"
214
+ write_json(
215
+ command_path,
216
+ {
217
+ "args": materialized,
218
+ "planned_args": planned_args,
219
+ "run_id": run.get("run_id"),
220
+ "lane_id": lane_id,
221
+ "reviewer_id": reviewer_id,
222
+ "command_metadata": command_metadata,
223
+ "context_pack_file": str(context_pack_path) if context_pack_path else "",
224
+ "dry_run": dry_run,
225
+ },
226
+ )
227
+ started = time.monotonic()
228
+ summary_path = summary_path_for_command(command)
229
+ resolved_summary_path = resolve_path_for_cwd(summary_path, cwd)
230
+ if not dry_run and resolved_summary_path is not None and resolved_summary_path.exists():
231
+ resolved_summary_path.unlink()
232
+ if dry_run:
233
+ returncode = None
234
+ stdout = ""
235
+ stderr = ""
236
+ duration_seconds = 0.0
237
+ else:
238
+ try:
239
+ completed = subprocess.run(
240
+ materialized,
241
+ capture_output=True,
242
+ text=True,
243
+ check=False,
244
+ cwd=str(cwd) if cwd is not None else None,
245
+ timeout=timeout_seconds,
246
+ )
247
+ except subprocess.TimeoutExpired as exc:
248
+ returncode = None
249
+ stdout = text_from_timeout_stream(exc.stdout)
250
+ stderr = text_from_timeout_stream(exc.stderr)
251
+ duration_seconds = time.monotonic() - started
252
+ command_status = "timeout"
253
+ except OSError as exc:
254
+ returncode = None
255
+ stdout = ""
256
+ stderr = f"{type(exc).__name__}: {exc}"
257
+ duration_seconds = time.monotonic() - started
258
+ command_status = "launch_failed"
259
+ else:
260
+ returncode = completed.returncode
261
+ stdout = completed.stdout
262
+ stderr = completed.stderr
263
+ duration_seconds = time.monotonic() - started
264
+ command_status = "finished"
265
+ stdout_path.write_text(stdout, encoding="utf-8")
266
+ stderr_path.write_text(stderr, encoding="utf-8")
267
+ summary_error = ""
268
+ try:
269
+ summary = None if dry_run else load_summary(resolved_summary_path)
270
+ except ValueError as exc:
271
+ summary = None
272
+ summary_error = str(exc)
273
+ command_result: dict[str, Any] = {
274
+ "run_id": run.get("run_id"),
275
+ "repo": run.get("repo"),
276
+ "pr_number": run.get("pr_number"),
277
+ "head_sha": run.get("head_sha"),
278
+ "arm_id": run.get("arm_id"),
279
+ "replicate": run.get("replicate"),
280
+ "command_index": command_index,
281
+ "lane_id": lane_id,
282
+ "reviewer_id": reviewer_id,
283
+ "command_metadata": command_metadata,
284
+ "status": "planned" if dry_run else command_status,
285
+ "returncode": returncode,
286
+ "duration_seconds": round(duration_seconds, 3),
287
+ "command_path": str(command_path),
288
+ "stdout_path": str(stdout_path),
289
+ "stderr_path": str(stderr_path),
290
+ "summary_path": str(summary_path) if summary_path is not None else "",
291
+ "summary_found": bool(summary),
292
+ }
293
+ if summary_error:
294
+ command_result["summary_error"] = summary_error
295
+ extracted: list[dict[str, Any]] = []
296
+ if summary is not None:
297
+ command_result["summary_mode"] = summary.get("mode")
298
+ try:
299
+ extracted = run_records_from_summary(
300
+ summary=summary,
301
+ item=corpus_item,
302
+ command_result=command_result,
303
+ )
304
+ except (TypeError, ValueError) as exc:
305
+ summary_error = str(exc)
306
+ command_result["summary_error"] = summary_error
307
+ command_result["extracted_reviewer_runs"] = len(extracted)
308
+ reviewer_runs.extend(extracted)
309
+ if not dry_run and not extracted:
310
+ infra_status = (
311
+ str(command_result["status"])
312
+ if command_result["status"] in {"timeout", "launch_failed"}
313
+ else "invalid_summary"
314
+ if summary is not None or summary_error
315
+ else "failed"
316
+ if returncode is not None and int(returncode) != 0
317
+ else "missing_summary"
318
+ )
319
+ infra_item = {**dict(corpus_item), **item}
320
+ infra_reviewers = (
321
+ local_llm_profiles_from_command(materialized)
322
+ if lane_id == "local-llm"
323
+ else []
324
+ ) or [reviewer_id]
325
+ reviewer_runs.extend(
326
+ infra_run_record(
327
+ lane_id=reviewer,
328
+ item=infra_item,
329
+ status=infra_status,
330
+ duration_seconds=duration_seconds,
331
+ artifact=str(result_dir / "result.json"),
332
+ )
333
+ for reviewer in infra_reviewers
334
+ )
335
+ write_json(result_dir / "result.json", command_result)
336
+ command_results.append(command_result)
337
+ executed += 1
338
+
339
+ payload = {
340
+ "mode": CALIBRATION_RUN_RESULTS_MODE,
341
+ "schema": CALIBRATION_RUN_RESULTS_SCHEMA,
342
+ "run_results_id": uuid.uuid4().hex,
343
+ "corpus_name": corpus.get("name", ""),
344
+ "started_at": started_at,
345
+ "finished_at": utc_now_iso(),
346
+ "replicates": replicates,
347
+ "output_dir": str(output_dir),
348
+ "results_dir": str(results_dir),
349
+ "context_pack_manifest": bool(context_pack_manifest),
350
+ "context_pack_output_dir": str(context_pack_output_dir),
351
+ "selected_lanes": sorted(selected_lanes),
352
+ "selected_arms": sorted(selected_arms),
353
+ "dry_run": dry_run,
354
+ "command_count": len(command_results),
355
+ "skipped_command_count": skipped,
356
+ "commands": command_results,
357
+ "reviewer_runs": reviewer_runs,
358
+ }
359
+ write_json(results_dir / "calibration-run-results.json", payload)
360
+ return payload
@@ -0,0 +1,188 @@
1
+ """Calibration truth normalization and expected-finding matching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any, Mapping
7
+
8
+
9
+ MATCH_TOKEN_RE = re.compile(r"[A-Za-z0-9_]+")
10
+ MATCH_STOPWORDS = {
11
+ "about",
12
+ "after",
13
+ "before",
14
+ "catch",
15
+ "caught",
16
+ "instead",
17
+ "issue",
18
+ "only",
19
+ "should",
20
+ "specific",
21
+ "that",
22
+ "this",
23
+ "until",
24
+ "with",
25
+ }
26
+ TRUTH_EXPECTATION_UNKNOWN = "unknown"
27
+ TRUTH_EXPECTATION_KNOWN_CLEAN = "known_clean"
28
+ TRUTH_EXPECTATION_KNOWN_BLOCKED = "known_blocked"
29
+ TRUTH_EXPECTATION_ALIASES = {
30
+ "blocked": TRUTH_EXPECTATION_KNOWN_BLOCKED,
31
+ "bug": TRUTH_EXPECTATION_KNOWN_BLOCKED,
32
+ "catch": TRUTH_EXPECTATION_KNOWN_BLOCKED,
33
+ "known-blocked": TRUTH_EXPECTATION_KNOWN_BLOCKED,
34
+ "known_blocked": TRUTH_EXPECTATION_KNOWN_BLOCKED,
35
+ "seeded-bug": TRUTH_EXPECTATION_KNOWN_BLOCKED,
36
+ "seeded_bug": TRUTH_EXPECTATION_KNOWN_BLOCKED,
37
+ "clean": TRUTH_EXPECTATION_KNOWN_CLEAN,
38
+ "known-clean": TRUTH_EXPECTATION_KNOWN_CLEAN,
39
+ "known_clean": TRUTH_EXPECTATION_KNOWN_CLEAN,
40
+ "no-blocker": TRUTH_EXPECTATION_KNOWN_CLEAN,
41
+ "no_blocker": TRUTH_EXPECTATION_KNOWN_CLEAN,
42
+ "pass": TRUTH_EXPECTATION_KNOWN_CLEAN,
43
+ "unknown": TRUTH_EXPECTATION_UNKNOWN,
44
+ }
45
+
46
+
47
+ def known_clean_source(source: str) -> bool:
48
+ return source.startswith("known-clean")
49
+
50
+
51
+ def known_blocked_source(source: str) -> bool:
52
+ return source.startswith("known-blocked") or source.startswith("seeded-bug")
53
+
54
+
55
+ def normalize_truth_expectation(value: Any) -> str:
56
+ expectation = str(value or "").strip().lower().replace("-", "_")
57
+ if not expectation:
58
+ return TRUTH_EXPECTATION_UNKNOWN
59
+ return TRUTH_EXPECTATION_ALIASES.get(expectation, TRUTH_EXPECTATION_UNKNOWN)
60
+
61
+
62
+ def truth_from_source(source: str) -> str:
63
+ if known_clean_source(source):
64
+ return TRUTH_EXPECTATION_KNOWN_CLEAN
65
+ if known_blocked_source(source):
66
+ return TRUTH_EXPECTATION_KNOWN_BLOCKED
67
+ return TRUTH_EXPECTATION_UNKNOWN
68
+
69
+
70
+ def normalize_truth(item: Mapping[str, Any], *, source: str | None = None) -> dict[str, Any]:
71
+ """Return the first-class calibration truth block for a corpus item.
72
+
73
+ Older corpora encoded ground truth in ``source`` prefixes and per-run
74
+ ``known_clean`` / ``known_blocked`` booleans. Keep those working, but prefer
75
+ an explicit ``truth.expectation`` field for new corpora so value reports do
76
+ not depend on naming conventions.
77
+ """
78
+
79
+ raw_truth = item.get("truth")
80
+ truth_mapping = raw_truth if isinstance(raw_truth, Mapping) else {}
81
+ expectation = normalize_truth_expectation(
82
+ truth_mapping.get("expectation")
83
+ or truth_mapping.get("expected_outcome")
84
+ or truth_mapping.get("outcome")
85
+ or truth_mapping.get("status")
86
+ )
87
+ if expectation == TRUTH_EXPECTATION_UNKNOWN:
88
+ if bool(item.get("known_clean")):
89
+ expectation = TRUTH_EXPECTATION_KNOWN_CLEAN
90
+ elif bool(item.get("known_blocked")):
91
+ expectation = TRUTH_EXPECTATION_KNOWN_BLOCKED
92
+ else:
93
+ expectation = truth_from_source(
94
+ str(source if source is not None else item.get("source") or "")
95
+ )
96
+ expected_findings = list(
97
+ truth_mapping.get("expected_findings")
98
+ or item.get("expected_findings")
99
+ or []
100
+ )
101
+ expected_themes = [
102
+ str(theme)
103
+ for theme in truth_mapping.get("expected_themes", []) or []
104
+ if str(theme).strip()
105
+ ]
106
+ return {
107
+ "expectation": expectation,
108
+ "known_clean": expectation == TRUTH_EXPECTATION_KNOWN_CLEAN,
109
+ "known_blocked": expectation == TRUTH_EXPECTATION_KNOWN_BLOCKED,
110
+ "expected_findings": expected_findings,
111
+ "expected_themes": expected_themes,
112
+ "notes": str(truth_mapping.get("notes") or ""),
113
+ }
114
+
115
+
116
+ def truth_for_item(item: Mapping[str, Any]) -> dict[str, Any]:
117
+ truth = item.get("truth")
118
+ if isinstance(truth, Mapping):
119
+ return normalize_truth(
120
+ {**dict(item), "truth": truth},
121
+ source=str(item.get("source") or ""),
122
+ )
123
+ return normalize_truth(item, source=str(item.get("source") or ""))
124
+
125
+
126
+ def _finding_path(finding: Mapping[str, Any]) -> str:
127
+ return str(finding.get("path") or finding.get("file") or finding.get("filename") or "")
128
+
129
+
130
+ def _finding_text(finding: Mapping[str, Any]) -> str:
131
+ parts = [
132
+ str(finding.get(key) or "").strip()
133
+ for key in ("summary", "text", "message", "body", "title", "detail")
134
+ ]
135
+ return " ".join(part for part in parts if part)
136
+
137
+
138
+ def _match_tokens(value: str) -> set[str]:
139
+ return {
140
+ token
141
+ for token in (match.group(0).lower() for match in MATCH_TOKEN_RE.finditer(value))
142
+ if len(token) > 2 and token not in MATCH_STOPWORDS
143
+ }
144
+
145
+
146
+ def _path_matches(expected_path: str, finding_path: str) -> bool:
147
+ if not expected_path:
148
+ return True
149
+ if not finding_path:
150
+ return False
151
+ expected = expected_path.strip().lower()
152
+ found = finding_path.strip().lower()
153
+ return found == expected or found.endswith(f"/{expected}") or expected.endswith(f"/{found}")
154
+
155
+
156
+ def _text_matches(expected_summary: str, finding_text: str) -> bool:
157
+ if not expected_summary:
158
+ return True
159
+ if not finding_text:
160
+ return False
161
+ expected_tokens = _match_tokens(expected_summary)
162
+ if not expected_tokens:
163
+ return expected_summary.lower() in finding_text.lower()
164
+ overlap = expected_tokens & _match_tokens(finding_text)
165
+ return len(overlap) >= min(2, len(expected_tokens))
166
+
167
+
168
+ def expected_finding_matches(expected_findings: Any, findings: Any) -> int:
169
+ if not isinstance(expected_findings, list) or not expected_findings:
170
+ return 0
171
+ if not isinstance(findings, list) or not findings:
172
+ return 0
173
+ matches = 0
174
+ for expected in expected_findings:
175
+ if not isinstance(expected, Mapping):
176
+ continue
177
+ expected_path = str(expected.get("path") or expected.get("file") or "")
178
+ expected_summary = str(expected.get("summary") or expected.get("text") or "")
179
+ for finding in findings:
180
+ if not isinstance(finding, Mapping):
181
+ continue
182
+ if _path_matches(expected_path, _finding_path(finding)) and _text_matches(
183
+ expected_summary,
184
+ _finding_text(finding),
185
+ ):
186
+ matches += 1
187
+ break
188
+ return matches
@@ -0,0 +1,142 @@
1
+ """Reviewer value report building and Markdown rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Iterable, Mapping
6
+
7
+ from .. import reviewer_metrics
8
+ from .evidence_report import build_reviewer_evidence_report
9
+ from .policy import build_lane_policy_report
10
+ from .run_results import corpus_with_run_results
11
+
12
+
13
+ def build_value_report(
14
+ corpus: Mapping[str, Any],
15
+ *,
16
+ spend: Mapping[str, Any] | None = None,
17
+ event_summaries: Iterable[Mapping[str, Any]] = (),
18
+ run_results: Iterable[Mapping[str, Any]] = (),
19
+ ) -> dict[str, Any]:
20
+ corpus = corpus_with_run_results(corpus, run_results)
21
+ evidence = build_reviewer_evidence_report(corpus)
22
+ metrics = reviewer_metrics.build_reviewer_metrics(
23
+ [evidence],
24
+ spend=spend,
25
+ event_summaries=event_summaries,
26
+ )
27
+ policy = build_lane_policy_report(metrics)
28
+ return {
29
+ "mode": "code-mower-reviewer-value-report",
30
+ "corpus_name": corpus.get("name", ""),
31
+ "description": corpus.get("description", ""),
32
+ "source_item_count": evidence["source_item_count"],
33
+ "evidence_count": evidence["evidence_count"],
34
+ "finding_evidence_count": evidence.get("finding_evidence_count", 0),
35
+ "run_disposition_count": evidence.get("run_disposition_count", 0),
36
+ "reviewer_run_count": len(evidence.get("reviewer_runs", [])),
37
+ "evidence": evidence,
38
+ "metrics": metrics,
39
+ "policy": policy,
40
+ }
41
+
42
+
43
+ def render_value_report_text(report: Mapping[str, Any]) -> str:
44
+ metrics = report.get("metrics", {})
45
+ policy = report.get("policy", {})
46
+ profiles = metrics.get("profiles", {}) if isinstance(metrics, Mapping) else {}
47
+ policies = policy.get("policies", {}) if isinstance(policy, Mapping) else {}
48
+ lines = [
49
+ "# Code Mower Reviewer Value Report",
50
+ "",
51
+ f"Corpus: `{report.get('corpus_name', '')}`",
52
+ f"Items: {report.get('source_item_count', 0)}",
53
+ f"Adjudicated evidence: {report.get('evidence_count', 0)}",
54
+ f"Finding evidence: {report.get('finding_evidence_count', 0)}",
55
+ f"Run dispositions: {report.get('run_disposition_count', 0)}",
56
+ f"Reviewer runs: {report.get('reviewer_run_count', 0)}",
57
+ "",
58
+ "| Reviewer | Runs | Useful | Negative | Useful rate | Known-clean pass | Known-blocked caught/missed | Infra errors | Input gaps | Cost | Sec/run | Cost/useful | Policy | Recommended role |",
59
+ "| --- | ---: | ---: | ---: | ---: | ---: | --- | ---: | ---: | ---: | ---: | ---: | --- | --- |",
60
+ ]
61
+ if isinstance(profiles, Mapping) and profiles:
62
+ for profile_id, stats in sorted(profiles.items()):
63
+ if not isinstance(stats, Mapping):
64
+ continue
65
+ profile_policy = policies.get(profile_id, {}) if isinstance(policies, Mapping) else {}
66
+ if not isinstance(profile_policy, Mapping):
67
+ profile_policy = {}
68
+ caught_missed = (
69
+ f"{stats.get('known_blocked_caught_runs', 0)}/"
70
+ f"{stats.get('known_blocked_missed_runs', 0)}"
71
+ )
72
+ useful_rate = stats.get("useful_rate")
73
+ useful_rate_text = "" if useful_rate is None else str(useful_rate)
74
+ lines.append(
75
+ "| "
76
+ + " | ".join(
77
+ [
78
+ f"`{profile_id}`",
79
+ str(stats.get("runs", 0)),
80
+ str(stats.get("useful_findings", 0)),
81
+ str(stats.get("negative_findings", 0)),
82
+ useful_rate_text,
83
+ str(stats.get("known_clean_pass_runs", 0)),
84
+ caught_missed,
85
+ str(stats.get("infra_error_runs", 0)),
86
+ str(stats.get("audit_input_insufficient_runs", 0)),
87
+ str(stats.get("cost_usd", 0)),
88
+ (
89
+ ""
90
+ if stats.get("seconds_per_run") is None
91
+ else str(stats.get("seconds_per_run"))
92
+ ),
93
+ (
94
+ ""
95
+ if stats.get("cost_per_useful_finding") is None
96
+ else str(stats.get("cost_per_useful_finding"))
97
+ ),
98
+ f"`{profile_policy.get('classification', '')}`",
99
+ f"`{profile_policy.get('recommended_role', '')}`",
100
+ ]
101
+ )
102
+ + " |"
103
+ )
104
+ else:
105
+ lines.append("| none | 0 | 0 | 0 | | 0 | 0/0 | 0 | 0 | 0 | | | | |")
106
+
107
+ recommendations = metrics.get("recommendations", []) if isinstance(metrics, Mapping) else []
108
+ lines.extend(["", "## Recommendations"])
109
+ if isinstance(recommendations, list) and recommendations:
110
+ lines.extend(f"- {item}" for item in recommendations)
111
+ else:
112
+ lines.append("- Collect more adjudicated reviewer evidence before changing merge policy.")
113
+
114
+ lines.extend(["", "## Policy Reasons"])
115
+ if isinstance(policies, Mapping) and policies:
116
+ for profile_id, profile_policy in sorted(policies.items()):
117
+ if not isinstance(profile_policy, Mapping):
118
+ continue
119
+ reasons = profile_policy.get("reasons", [])
120
+ reason_text = (
121
+ "; ".join(str(reason) for reason in reasons)
122
+ if isinstance(reasons, list)
123
+ else ""
124
+ )
125
+ trigger_classes = profile_policy.get("suggested_trigger_classes", [])
126
+ trigger_text = (
127
+ f"; suggested classes: {', '.join(str(item) for item in trigger_classes)}"
128
+ if isinstance(trigger_classes, list) and trigger_classes
129
+ else ""
130
+ )
131
+ lines.append(
132
+ f"- `{profile_id}`: `{profile_policy.get('classification', '')}`"
133
+ f" / `{profile_policy.get('recommended_role', '')}`"
134
+ f" / `{profile_policy.get('automatic_trigger', '')}`"
135
+ + (f" - {reason_text}{trigger_text}" if reason_text or trigger_text else "")
136
+ )
137
+ else:
138
+ lines.append("- No policy rows available.")
139
+ caveat = policy.get("caveat") if isinstance(policy, Mapping) else None
140
+ if caveat:
141
+ lines.extend(["", f"_Caveat: {caveat}_"])
142
+ return "\n".join(lines) + "\n"