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,315 @@
1
+ """Calibration reviewer result normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+
7
+ from .evidence import NON_BLOCKING_CODERABBIT_SEVERITIES
8
+ from .run_status import (
9
+ RUN_STATUS_AUDIT_INPUT_INSUFFICIENT,
10
+ RUN_STATUS_BLOCKED,
11
+ RUN_STATUS_INFRA_ERROR,
12
+ RUN_STATUS_PASS,
13
+ count_normalized_findings,
14
+ normalize_run_status_category,
15
+ status_from_verdict,
16
+ )
17
+ from .truth import expected_finding_matches, truth_for_item
18
+
19
+
20
+ AUDIT_INPUT_INSUFFICIENT_PATTERNS = (
21
+ "audit input incomplete",
22
+ "audit input is incomplete",
23
+ "audit input was incomplete",
24
+ "diff is incomplete",
25
+ "diff was incomplete",
26
+ "diff was truncated",
27
+ "diff truncation",
28
+ "incomplete diff",
29
+ "incomplete review context",
30
+ "insufficient audit input",
31
+ "review context is incomplete",
32
+ "truncated diff",
33
+ )
34
+
35
+
36
+ def _finding_is_audit_input_insufficient(finding: Mapping[str, Any]) -> bool:
37
+ text = " ".join(
38
+ str(finding.get(key) or "").strip().lower()
39
+ for key in ("summary", "text", "message", "body", "title", "detail")
40
+ )
41
+ return any(pattern in text for pattern in AUDIT_INPUT_INSUFFICIENT_PATTERNS)
42
+
43
+
44
+ def audit_input_insufficient_result(findings: Any) -> bool:
45
+ if not isinstance(findings, list):
46
+ return False
47
+ blockers = [
48
+ finding
49
+ for finding in findings
50
+ if isinstance(finding, Mapping)
51
+ and str(finding.get("severity") or "").strip().upper() in {"P0", "P1", "P2"}
52
+ ]
53
+ return bool(blockers) and all(
54
+ _finding_is_audit_input_insufficient(finding) for finding in blockers
55
+ )
56
+
57
+
58
+ def coderabbit_blocking_findings(findings: Any) -> list[Mapping[str, Any]]:
59
+ if not isinstance(findings, list):
60
+ return []
61
+ blocking: list[Mapping[str, Any]] = []
62
+ for finding in findings:
63
+ if not isinstance(finding, Mapping):
64
+ continue
65
+ severity = str(finding.get("severity") or "").strip().lower()
66
+ if severity in NON_BLOCKING_CODERABBIT_SEVERITIES:
67
+ continue
68
+ blocking.append(finding)
69
+ return blocking
70
+
71
+
72
+ def local_llm_findings(run: Mapping[str, Any]) -> list[dict[str, Any]]:
73
+ findings: list[dict[str, Any]] = []
74
+ for finding_group in run.get("blocker_findings", []) or []:
75
+ if not isinstance(finding_group, Mapping):
76
+ continue
77
+ path = str(finding_group.get("path") or "")
78
+ for text in finding_group.get("blockers", []) or []:
79
+ findings.append({"path": path, "text": str(text)})
80
+ for text in run.get("pr_level_blockers", []) or []:
81
+ findings.append({"path": "__pr__", "text": str(text)})
82
+ for finding_group in run.get("concern_findings", []) or []:
83
+ if not isinstance(finding_group, Mapping):
84
+ continue
85
+ path = str(finding_group.get("path") or "")
86
+ for text in finding_group.get("concerns", []) or []:
87
+ findings.append({"path": path, "text": str(text)})
88
+ return findings
89
+
90
+
91
+ def observed_head_field(observed_head_sha: str, head_sha: str) -> dict[str, str]:
92
+ if observed_head_sha and observed_head_sha != head_sha:
93
+ return {"observed_head_sha": observed_head_sha}
94
+ return {}
95
+
96
+
97
+ def run_records_from_summary(
98
+ *,
99
+ summary: Mapping[str, Any],
100
+ item: Mapping[str, Any],
101
+ command_result: Mapping[str, Any],
102
+ ) -> list[dict[str, Any]]:
103
+ artifact = str(command_result.get("summary_path") or "")
104
+ returncode = command_result.get("returncode")
105
+ try:
106
+ returncode_int = int(returncode) if returncode is not None else None
107
+ except (TypeError, ValueError):
108
+ returncode_int = None
109
+ mode = str(summary.get("mode") or "")
110
+ repo = str(summary.get("repo") or item.get("repo") or "")
111
+ pr_number = int(summary.get("pr_number") or item.get("pr_number") or 0)
112
+ head_sha = str(item.get("head_sha") or "")
113
+ observed_head_sha = str(summary.get("head_sha") or "")
114
+ truth = truth_for_item(item)
115
+ known_clean = bool(truth.get("known_clean"))
116
+ known_blocked = bool(truth.get("known_blocked"))
117
+ expected_findings = item.get("expected_findings", [])
118
+ calibration_run_id = str(command_result.get("run_id") or "")
119
+ replicate = command_result.get("replicate")
120
+
121
+ def expected_matches(status: str, finding_count: int, findings: Any = None) -> int:
122
+ if not known_blocked or status != RUN_STATUS_BLOCKED or finding_count <= 0:
123
+ return 0
124
+ if expected_findings:
125
+ return expected_finding_matches(expected_findings, findings)
126
+ return 1
127
+
128
+ if mode in {"antigravity-cli-audit", "gemini-cli-audit", "hermes-cli-audit"}:
129
+ default_reviewer = (
130
+ "antigravity-cli"
131
+ if mode == "antigravity-cli-audit"
132
+ else "hermes-cli"
133
+ if mode == "hermes-cli-audit"
134
+ else "gemini-cli"
135
+ )
136
+ reviewer_id = str(command_result.get("reviewer_id") or default_reviewer)
137
+ verdict = summary.get("verdict", {})
138
+ if not isinstance(verdict, Mapping):
139
+ verdict = {}
140
+ findings = verdict.get("findings", [])
141
+ parse_failed = bool(verdict.get("parse_failed"))
142
+ parse_status = "parse_failed" if parse_failed else "json"
143
+ result_category = normalize_run_status_category(verdict.get("result_category"))
144
+ audit_input_insufficient = (
145
+ result_category == RUN_STATUS_AUDIT_INPUT_INSUFFICIENT
146
+ or audit_input_insufficient_result(findings)
147
+ )
148
+ if parse_failed:
149
+ status = RUN_STATUS_INFRA_ERROR
150
+ elif audit_input_insufficient:
151
+ status = RUN_STATUS_AUDIT_INPUT_INSUFFICIENT
152
+ else:
153
+ status = status_from_verdict(verdict.get("verdict"), returncode=returncode_int)
154
+ finding_count = count_normalized_findings(findings)
155
+ return [
156
+ {
157
+ "reviewer": reviewer_id,
158
+ "status": status,
159
+ "finding_count": finding_count,
160
+ "expected_finding_matches": expected_matches(status, finding_count, findings),
161
+ "result_category": status if audit_input_insufficient else "",
162
+ "audit_input_insufficient_count": 1 if audit_input_insufficient else 0,
163
+ "known_clean": known_clean,
164
+ "known_blocked": known_blocked,
165
+ "duration_seconds": summary.get("duration_seconds"),
166
+ "parse_status": parse_status,
167
+ "artifact": artifact,
168
+ "model": summary.get("model") or "",
169
+ "repo": repo,
170
+ "pr_number": pr_number,
171
+ "head_sha": head_sha,
172
+ **observed_head_field(observed_head_sha, head_sha),
173
+ "calibration_run_id": calibration_run_id,
174
+ "replicate": replicate,
175
+ }
176
+ ]
177
+ if mode == "coderabbit-cli-audit":
178
+ reviewer_id = str(command_result.get("reviewer_id") or "coderabbit-cli")
179
+ try:
180
+ raw_finding_count = int(summary.get("finding_count") or 0)
181
+ except (TypeError, ValueError) as exc:
182
+ raise ValueError("invalid coderabbit finding_count") from exc
183
+ findings = summary.get("findings", [])
184
+ if isinstance(findings, list) and findings:
185
+ parsed_finding_count = count_normalized_findings(findings)
186
+ blocking_findings = coderabbit_blocking_findings(findings)
187
+ if raw_finding_count > parsed_finding_count:
188
+ finding_count = raw_finding_count
189
+ else:
190
+ finding_count = len(blocking_findings)
191
+ else:
192
+ parsed_finding_count = 0
193
+ blocking_findings = []
194
+ finding_count = raw_finding_count
195
+ parse_status = str(summary.get("parse_status") or "").strip().lower()
196
+ head_check = summary.get("head_check", {})
197
+ head_check_status = (
198
+ str(head_check.get("status") or "")
199
+ if isinstance(head_check, Mapping)
200
+ else ""
201
+ )
202
+ if parse_status in {"raw", "parse_failed"}:
203
+ status = RUN_STATUS_INFRA_ERROR
204
+ elif head_check_status and head_check_status != "pass":
205
+ status = RUN_STATUS_INFRA_ERROR
206
+ elif returncode_int not in {None, 0} and not finding_count:
207
+ status = RUN_STATUS_INFRA_ERROR
208
+ else:
209
+ status = RUN_STATUS_BLOCKED if finding_count else RUN_STATUS_PASS
210
+ record = {
211
+ "reviewer": reviewer_id,
212
+ "status": status,
213
+ "finding_count": finding_count,
214
+ "expected_finding_matches": expected_matches(
215
+ status,
216
+ finding_count,
217
+ blocking_findings,
218
+ ),
219
+ "known_clean": known_clean,
220
+ "known_blocked": known_blocked,
221
+ "duration_seconds": summary.get("duration_seconds"),
222
+ "parse_status": parse_status,
223
+ "artifact": artifact,
224
+ "repo": repo,
225
+ "pr_number": pr_number,
226
+ "head_sha": head_sha,
227
+ **observed_head_field(observed_head_sha, head_sha),
228
+ "calibration_run_id": calibration_run_id,
229
+ "replicate": replicate,
230
+ }
231
+ if raw_finding_count != finding_count:
232
+ record["raw_finding_count"] = raw_finding_count
233
+ if parsed_finding_count != raw_finding_count:
234
+ record["parsed_finding_count"] = parsed_finding_count
235
+ if raw_finding_count > parsed_finding_count:
236
+ record["unparsed_finding_count"] = raw_finding_count - parsed_finding_count
237
+ non_blocking_finding_count = parsed_finding_count - len(blocking_findings)
238
+ if non_blocking_finding_count:
239
+ record["non_blocking_finding_count"] = non_blocking_finding_count
240
+ return [record]
241
+ if mode == "local-llm-bakeoff":
242
+ records: list[dict[str, Any]] = []
243
+ for run in summary.get("runs", []) or []:
244
+ if not isinstance(run, Mapping):
245
+ continue
246
+ finding_count = (
247
+ int(run.get("blocker_file_count") or 0)
248
+ + int(run.get("concern_file_count") or 0)
249
+ + len(run.get("pr_level_blockers", []) or [])
250
+ )
251
+ findings = local_llm_findings(run)
252
+ status = status_from_verdict(run.get("verdict"), returncode=returncode_int)
253
+ records.append(
254
+ {
255
+ "reviewer": str(run.get("profile_id") or "local-llm"),
256
+ "status": status,
257
+ "finding_count": finding_count,
258
+ "expected_finding_matches": expected_matches(
259
+ status,
260
+ finding_count,
261
+ findings,
262
+ ),
263
+ "known_clean": known_clean,
264
+ "known_blocked": known_blocked,
265
+ "duration_seconds": run.get("duration_seconds"),
266
+ "parse_status": (
267
+ "parse_failed"
268
+ if int(run.get("parse_failure_count") or 0)
269
+ else "json"
270
+ ),
271
+ "artifact": artifact,
272
+ "model": str(run.get("model") or ""),
273
+ "repo": str(run.get("repo") or repo),
274
+ "pr_number": int(run.get("pr_number") or pr_number),
275
+ "head_sha": head_sha,
276
+ **observed_head_field(
277
+ str(run.get("head_sha_end") or observed_head_sha),
278
+ head_sha,
279
+ ),
280
+ "calibration_run_id": calibration_run_id,
281
+ "replicate": replicate,
282
+ }
283
+ )
284
+ return records
285
+ return []
286
+
287
+
288
+ def infra_run_record(
289
+ *,
290
+ lane_id: str,
291
+ item: Mapping[str, Any],
292
+ status: str,
293
+ duration_seconds: float,
294
+ artifact: str,
295
+ ) -> dict[str, Any]:
296
+ reviewer = lane_id
297
+ if reviewer == "local-llm":
298
+ reviewer = "local-llm"
299
+ truth = truth_for_item(item)
300
+ return {
301
+ "reviewer": reviewer,
302
+ "status": status,
303
+ "finding_count": 0,
304
+ "expected_finding_matches": 0,
305
+ "known_clean": bool(truth.get("known_clean")),
306
+ "known_blocked": bool(truth.get("known_blocked")),
307
+ "duration_seconds": round(duration_seconds, 3),
308
+ "parse_status": status,
309
+ "artifact": artifact,
310
+ "repo": str(item.get("repo") or ""),
311
+ "pr_number": int(item.get("pr_number") or 0),
312
+ "head_sha": str(item.get("head_sha") or ""),
313
+ "calibration_run_id": str(item.get("calibration_run_id") or ""),
314
+ "replicate": item.get("replicate"),
315
+ }
@@ -0,0 +1,147 @@
1
+ """Calibration run-results ingestion and corpus folding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from pathlib import Path
7
+ from typing import Any, Iterable, Mapping, Sequence
8
+
9
+ from .corpus import load_json_object
10
+ from .evidence import KNOWN_EVIDENCE_DISPOSITIONS
11
+ from .run_status import normalize_run_status_category
12
+
13
+
14
+ CALIBRATION_RUN_RESULTS_MODE = "code-mower-calibration-run-results"
15
+ CALIBRATION_RUN_RESULTS_SCHEMA = "code_mower.calibrationRunResults.v1"
16
+
17
+
18
+ def load_run_results(paths: Iterable[Path]) -> list[Mapping[str, Any]]:
19
+ reports: list[Mapping[str, Any]] = []
20
+ for path in paths:
21
+ payload = load_json_object(path)
22
+ if payload.get("mode") != CALIBRATION_RUN_RESULTS_MODE:
23
+ raise ValueError(f"{path} is not a Code Mower calibration run-results file")
24
+ reports.append(payload)
25
+ return reports
26
+
27
+
28
+ def csv_values(value: Any) -> set[str]:
29
+ if isinstance(value, str):
30
+ return {part.strip() for part in value.split(",") if part.strip()}
31
+ if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)):
32
+ return {str(part).strip() for part in value if str(part).strip()}
33
+ if value is None:
34
+ return set()
35
+ return {str(value).strip()} if str(value).strip() else set()
36
+
37
+
38
+ def normalize_disposition(value: Any) -> str:
39
+ disposition = str(value or "unknown").strip().lower().replace("-", "_")
40
+ if disposition not in KNOWN_EVIDENCE_DISPOSITIONS:
41
+ return "unknown"
42
+ return disposition
43
+
44
+
45
+ def run_matches_disposition_rule(
46
+ run: Mapping[str, Any],
47
+ rule: Mapping[str, Any],
48
+ ) -> bool:
49
+ reviewers = csv_values(rule.get("reviewer") or rule.get("profile_id") or rule.get("lane"))
50
+ if reviewers:
51
+ reviewer = str(
52
+ run.get("reviewer")
53
+ or run.get("profile_id")
54
+ or run.get("lane")
55
+ or "unknown-reviewer"
56
+ ).strip()
57
+ if reviewer not in reviewers:
58
+ return False
59
+ statuses = {
60
+ normalize_run_status_category(status)
61
+ for status in csv_values(rule.get("status") or rule.get("status_category"))
62
+ }
63
+ if statuses:
64
+ run_status = normalize_run_status_category(run.get("status") or run.get("verdict"))
65
+ if run_status not in statuses:
66
+ return False
67
+ result_categories = csv_values(rule.get("result_category"))
68
+ if result_categories:
69
+ if str(run.get("result_category") or "").strip() not in result_categories:
70
+ return False
71
+ try:
72
+ min_findings = int(rule.get("min_finding_count") or 0)
73
+ except (TypeError, ValueError):
74
+ min_findings = 0
75
+ if min_findings:
76
+ try:
77
+ finding_count = int(run.get("finding_count") or 0)
78
+ except (TypeError, ValueError):
79
+ finding_count = 0
80
+ if finding_count < min_findings:
81
+ return False
82
+ return True
83
+
84
+
85
+ def apply_run_disposition_rules(run: dict[str, Any], item: Mapping[str, Any]) -> None:
86
+ for rule in item.get("reviewer_run_dispositions", []) or []:
87
+ if not isinstance(rule, Mapping):
88
+ continue
89
+ if not run_matches_disposition_rule(run, rule):
90
+ continue
91
+ disposition = normalize_disposition(rule.get("disposition"))
92
+ if disposition != "unknown" and not run.get("disposition"):
93
+ run["disposition"] = disposition
94
+ if rule.get("expected_blocker_caught") is not None:
95
+ run["expected_blocker_caught"] = bool(rule.get("expected_blocker_caught"))
96
+ if rule.get("notes") and not run.get("disposition_notes"):
97
+ run["disposition_notes"] = str(rule.get("notes") or "")
98
+ # Apply the first matching adjudication rule. Additional notes can be
99
+ # modeled as reviewer_evidence if a corpus needs more detail.
100
+ return
101
+
102
+
103
+ def corpus_with_run_results(
104
+ corpus: Mapping[str, Any],
105
+ run_results: Iterable[Mapping[str, Any]],
106
+ ) -> dict[str, Any]:
107
+ merged = copy.deepcopy(dict(corpus))
108
+ items = merged.get("corpus", [])
109
+ if not isinstance(items, list):
110
+ raise ValueError("calibration corpus must include a corpus list")
111
+ item_by_key: dict[tuple[str, int, str], dict[str, Any]] = {}
112
+ for item in items:
113
+ if not isinstance(item, dict):
114
+ continue
115
+ key = (
116
+ str(item.get("repo") or ""),
117
+ int(item.get("pr_number") or 0),
118
+ str(item.get("head_sha") or ""),
119
+ )
120
+ item_by_key[key] = item
121
+
122
+ for result_index, result_report in enumerate(run_results):
123
+ manifest_id = str(
124
+ result_report.get("run_results_id")
125
+ or result_report.get("started_at")
126
+ or result_index
127
+ )
128
+ for run in result_report.get("reviewer_runs", []) or []:
129
+ if not isinstance(run, Mapping):
130
+ continue
131
+ key = (
132
+ str(run.get("repo") or ""),
133
+ int(run.get("pr_number") or 0),
134
+ str(run.get("head_sha") or ""),
135
+ )
136
+ item = item_by_key.get(key)
137
+ if item is None:
138
+ continue
139
+ folded_run = {
140
+ key: value
141
+ for key, value in run.items()
142
+ if key not in {"repo", "pr_number", "head_sha"}
143
+ }
144
+ folded_run.setdefault("calibration_manifest_id", manifest_id)
145
+ apply_run_disposition_rules(folded_run, item)
146
+ item.setdefault("reviewer_runs", []).append(folded_run)
147
+ return merged
@@ -0,0 +1,64 @@
1
+ """Reviewer run status normalization for calibration reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+
7
+
8
+ RUN_STATUS_PASS = "pass"
9
+ RUN_STATUS_BLOCKED = "blocked"
10
+ RUN_STATUS_AUDIT_INPUT_INSUFFICIENT = "audit_input_insufficient"
11
+ RUN_STATUS_INFRA_ERROR = "infra_error"
12
+ RUN_STATUS_UNKNOWN = "unknown"
13
+
14
+ RUN_STATUS_CATEGORY_ALIASES = {
15
+ "pass": RUN_STATUS_PASS,
16
+ "passed": RUN_STATUS_PASS,
17
+ "complete": RUN_STATUS_PASS,
18
+ "completed": RUN_STATUS_PASS,
19
+ "done": RUN_STATUS_PASS,
20
+ "success": RUN_STATUS_PASS,
21
+ "succeeded": RUN_STATUS_PASS,
22
+ "block": RUN_STATUS_BLOCKED,
23
+ "blocked": RUN_STATUS_BLOCKED,
24
+ "audit_input_insufficient": RUN_STATUS_AUDIT_INPUT_INSUFFICIENT,
25
+ "context_insufficient": RUN_STATUS_AUDIT_INPUT_INSUFFICIENT,
26
+ "fail": RUN_STATUS_BLOCKED,
27
+ "error": RUN_STATUS_INFRA_ERROR,
28
+ "failed": RUN_STATUS_INFRA_ERROR,
29
+ "failure": RUN_STATUS_INFRA_ERROR,
30
+ "infra_error": RUN_STATUS_INFRA_ERROR,
31
+ "input_insufficient": RUN_STATUS_AUDIT_INPUT_INSUFFICIENT,
32
+ "insufficient_context": RUN_STATUS_AUDIT_INPUT_INSUFFICIENT,
33
+ "invalid_summary": RUN_STATUS_INFRA_ERROR,
34
+ "launch_failed": RUN_STATUS_INFRA_ERROR,
35
+ "missing_summary": RUN_STATUS_INFRA_ERROR,
36
+ "rate_limit": RUN_STATUS_INFRA_ERROR,
37
+ "rate_limited": RUN_STATUS_INFRA_ERROR,
38
+ "setup_error": RUN_STATUS_INFRA_ERROR,
39
+ "stale": RUN_STATUS_INFRA_ERROR,
40
+ "timeout": RUN_STATUS_INFRA_ERROR,
41
+ "timed_out": RUN_STATUS_INFRA_ERROR,
42
+ }
43
+
44
+
45
+ def normalize_run_status_category(value: Any) -> str:
46
+ """Return the semantic category used for reviewer-run policy decisions."""
47
+
48
+ status = str(value or "").strip().lower().replace("-", "_")
49
+ return RUN_STATUS_CATEGORY_ALIASES.get(status, RUN_STATUS_UNKNOWN)
50
+
51
+
52
+ def status_from_verdict(value: Any, *, returncode: int | None = None) -> str:
53
+ category = normalize_run_status_category(value)
54
+ if category != RUN_STATUS_UNKNOWN:
55
+ return category
56
+ if returncode is not None and returncode != 0:
57
+ return RUN_STATUS_INFRA_ERROR
58
+ return RUN_STATUS_UNKNOWN
59
+
60
+
61
+ def count_normalized_findings(findings: Any) -> int:
62
+ if not isinstance(findings, list):
63
+ return 0
64
+ return sum(1 for finding in findings if isinstance(finding, Mapping))