code-mower 0.5.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Report reviewer accuracy, spend, and value from adjudicated outputs."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from collections import Counter, defaultdict
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Iterable, Mapping
|
|
12
|
+
|
|
13
|
+
if __package__ in {None, ""}:
|
|
14
|
+
module_dir = Path(__file__).resolve().parent
|
|
15
|
+
sys.path.insert(0, str(module_dir.parent))
|
|
16
|
+
if module_dir.name == "code_mower": # pragma: no cover - extracted direct CLI.
|
|
17
|
+
from code_mower import code_mower_telemetry
|
|
18
|
+
else:
|
|
19
|
+
from tools import code_mower_telemetry
|
|
20
|
+
elif __package__ == "tools":
|
|
21
|
+
from tools import code_mower_telemetry
|
|
22
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
23
|
+
from . import code_mower_telemetry
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
POSITIVE_DISPOSITIONS = {"true_positive", "useful"}
|
|
27
|
+
NEGATIVE_DISPOSITIONS = {"false_positive", "noise"}
|
|
28
|
+
KNOWN_DISPOSITIONS = POSITIVE_DISPOSITIONS | NEGATIVE_DISPOSITIONS | {"unknown"}
|
|
29
|
+
SUPPORTED_CALIBRATION_REPORT_MODES = {
|
|
30
|
+
"local-llm-calibration",
|
|
31
|
+
"reviewer-evidence-calibration",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_json(path: Path) -> Mapping[str, Any]:
|
|
36
|
+
try:
|
|
37
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
38
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
39
|
+
raise ValueError(f"unable to read {path}: {exc}") from exc
|
|
40
|
+
if not isinstance(payload, Mapping):
|
|
41
|
+
raise ValueError(f"{path} must contain a JSON object")
|
|
42
|
+
return payload
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_event_summary(path: Path) -> Mapping[str, Any]:
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
raise ValueError(f"event log does not exist or is not a file: {path}")
|
|
48
|
+
return code_mower_telemetry.summarize_events(
|
|
49
|
+
code_mower_telemetry.load_jsonl_events(path)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _float(value: Any) -> float:
|
|
54
|
+
if isinstance(value, bool):
|
|
55
|
+
return 0.0
|
|
56
|
+
if isinstance(value, (int, float)):
|
|
57
|
+
return float(value)
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
try:
|
|
60
|
+
return float(value)
|
|
61
|
+
except ValueError:
|
|
62
|
+
return 0.0
|
|
63
|
+
return 0.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _spend_by_profile(spend_payload: Mapping[str, Any] | None) -> dict[str, float]:
|
|
67
|
+
if not spend_payload:
|
|
68
|
+
return {}
|
|
69
|
+
raw_profiles = spend_payload.get("profiles", spend_payload)
|
|
70
|
+
if not isinstance(raw_profiles, Mapping):
|
|
71
|
+
raise ValueError("spend file must be a mapping or contain a profiles mapping")
|
|
72
|
+
spend: dict[str, float] = {}
|
|
73
|
+
for profile_id, value in raw_profiles.items():
|
|
74
|
+
if isinstance(value, Mapping):
|
|
75
|
+
cost = _float(value.get("cost_usd", value.get("usd")))
|
|
76
|
+
else:
|
|
77
|
+
cost = _float(value)
|
|
78
|
+
spend[str(profile_id)] = cost
|
|
79
|
+
return spend
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _rate(numerator: float, denominator: float) -> float | None:
|
|
83
|
+
if denominator <= 0:
|
|
84
|
+
return None
|
|
85
|
+
return round(numerator / denominator, 4)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_reviewer_metrics(
|
|
89
|
+
calibration_reports: Iterable[Mapping[str, Any]],
|
|
90
|
+
*,
|
|
91
|
+
spend: Mapping[str, Any] | None = None,
|
|
92
|
+
event_summaries: Iterable[Mapping[str, Any]] = (),
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
spend_map = _spend_by_profile(spend)
|
|
95
|
+
counts: dict[str, Counter[str]] = defaultdict(Counter)
|
|
96
|
+
durations: dict[str, float] = defaultdict(float)
|
|
97
|
+
runs: dict[str, int] = defaultdict(int)
|
|
98
|
+
known_clean_pass_runs: dict[str, int] = defaultdict(int)
|
|
99
|
+
blocking_false_positive_runs: dict[str, int] = defaultdict(int)
|
|
100
|
+
known_blocked_runs: dict[str, int] = defaultdict(int)
|
|
101
|
+
known_blocked_caught_runs: dict[str, int] = defaultdict(int)
|
|
102
|
+
known_blocked_missed_runs: dict[str, int] = defaultdict(int)
|
|
103
|
+
infra_error_runs: dict[str, int] = defaultdict(int)
|
|
104
|
+
audit_input_insufficient_runs: dict[str, int] = defaultdict(int)
|
|
105
|
+
run_statuses: dict[str, Counter[str]] = defaultdict(Counter)
|
|
106
|
+
review_classes: dict[str, set[str]] = defaultdict(set)
|
|
107
|
+
context_packs: dict[str, set[str]] = defaultdict(set)
|
|
108
|
+
useful_review_classes: dict[str, set[str]] = defaultdict(set)
|
|
109
|
+
useful_context_packs: dict[str, set[str]] = defaultdict(set)
|
|
110
|
+
models: dict[str, str] = {}
|
|
111
|
+
source_count = 0
|
|
112
|
+
event_summary_count = 0
|
|
113
|
+
|
|
114
|
+
for report in calibration_reports:
|
|
115
|
+
source_count += 1
|
|
116
|
+
if report.get("mode") not in SUPPORTED_CALIBRATION_REPORT_MODES:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"reviewer metrics expects local-llm-calibration or "
|
|
119
|
+
"reviewer-evidence-calibration reports"
|
|
120
|
+
)
|
|
121
|
+
profiles = report.get("profiles", {})
|
|
122
|
+
if isinstance(profiles, Mapping):
|
|
123
|
+
for profile_id, stats in profiles.items():
|
|
124
|
+
if not isinstance(stats, Mapping):
|
|
125
|
+
continue
|
|
126
|
+
profile = str(profile_id)
|
|
127
|
+
models[profile] = str(stats.get("model") or models.get(profile, ""))
|
|
128
|
+
runs[profile] += int(stats.get("runs") or 0)
|
|
129
|
+
durations[profile] += _float(stats.get("duration_seconds_total"))
|
|
130
|
+
known_clean_pass_runs[profile] += int(stats.get("known_clean_pass_runs") or 0)
|
|
131
|
+
blocking_false_positive_runs[profile] += int(
|
|
132
|
+
stats.get("blocking_false_positive_runs") or 0
|
|
133
|
+
)
|
|
134
|
+
known_blocked_runs[profile] += int(stats.get("known_blocked_runs") or 0)
|
|
135
|
+
known_blocked_caught_runs[profile] += int(
|
|
136
|
+
stats.get("known_blocked_caught_runs") or 0
|
|
137
|
+
)
|
|
138
|
+
known_blocked_missed_runs[profile] += int(
|
|
139
|
+
stats.get("known_blocked_missed_runs") or 0
|
|
140
|
+
)
|
|
141
|
+
infra_error_runs[profile] += int(stats.get("infra_error_runs") or 0)
|
|
142
|
+
audit_input_insufficient_runs[profile] += int(
|
|
143
|
+
stats.get("audit_input_insufficient_runs") or 0
|
|
144
|
+
)
|
|
145
|
+
raw_run_statuses = stats.get("run_statuses", {})
|
|
146
|
+
if isinstance(raw_run_statuses, Mapping):
|
|
147
|
+
for status, count in raw_run_statuses.items():
|
|
148
|
+
run_statuses[profile][str(status)] += int(count or 0)
|
|
149
|
+
for review_class in stats.get("review_classes", []) or []:
|
|
150
|
+
text = str(review_class).strip()
|
|
151
|
+
if text:
|
|
152
|
+
review_classes[profile].add(text)
|
|
153
|
+
for context_pack in stats.get("context_packs", []) or []:
|
|
154
|
+
text = str(context_pack).strip()
|
|
155
|
+
if text:
|
|
156
|
+
context_packs[profile].add(text)
|
|
157
|
+
for review_class in stats.get("useful_review_classes", []) or []:
|
|
158
|
+
text = str(review_class).strip()
|
|
159
|
+
if text:
|
|
160
|
+
useful_review_classes[profile].add(text)
|
|
161
|
+
for context_pack in stats.get("useful_context_packs", []) or []:
|
|
162
|
+
text = str(context_pack).strip()
|
|
163
|
+
if text:
|
|
164
|
+
useful_context_packs[profile].add(text)
|
|
165
|
+
dispositions = stats.get("dispositions", {})
|
|
166
|
+
if isinstance(dispositions, Mapping):
|
|
167
|
+
for disposition, count in dispositions.items():
|
|
168
|
+
normalized = str(disposition).strip().lower()
|
|
169
|
+
if normalized not in KNOWN_DISPOSITIONS:
|
|
170
|
+
continue
|
|
171
|
+
counts[profile][normalized] += int(count or 0)
|
|
172
|
+
|
|
173
|
+
lane_events: dict[str, dict[str, Any]] = {}
|
|
174
|
+
for summary in event_summaries:
|
|
175
|
+
event_summary_count += 1
|
|
176
|
+
if summary.get("mode") != "telemetry-summary":
|
|
177
|
+
raise ValueError("event summaries must be telemetry-summary reports")
|
|
178
|
+
lanes = summary.get("lanes", {})
|
|
179
|
+
if not isinstance(lanes, Mapping):
|
|
180
|
+
continue
|
|
181
|
+
for lane_id, lane_stats in lanes.items():
|
|
182
|
+
if not isinstance(lane_stats, Mapping):
|
|
183
|
+
continue
|
|
184
|
+
profile = str(lane_id)
|
|
185
|
+
existing = lane_events.setdefault(
|
|
186
|
+
profile,
|
|
187
|
+
{
|
|
188
|
+
"events": 0,
|
|
189
|
+
"finished": 0,
|
|
190
|
+
"pass": 0,
|
|
191
|
+
"blocked": 0,
|
|
192
|
+
"failed": 0,
|
|
193
|
+
"findings": 0,
|
|
194
|
+
"observed_pr_count": 0,
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
for key in ("events", "finished", "pass", "blocked", "failed", "findings", "observed_pr_count"):
|
|
198
|
+
existing[key] += int(lane_stats.get(key) or 0)
|
|
199
|
+
|
|
200
|
+
profiles_out: dict[str, dict[str, Any]] = {}
|
|
201
|
+
for profile_id in sorted(set(counts) | set(runs) | set(spend_map) | set(lane_events)):
|
|
202
|
+
profile_counts = counts[profile_id]
|
|
203
|
+
true_positive = profile_counts["true_positive"]
|
|
204
|
+
false_positive = profile_counts["false_positive"]
|
|
205
|
+
useful = profile_counts["useful"]
|
|
206
|
+
noise = profile_counts["noise"]
|
|
207
|
+
unknown = profile_counts["unknown"]
|
|
208
|
+
known = true_positive + false_positive + useful + noise
|
|
209
|
+
useful_total = true_positive + useful
|
|
210
|
+
negative_total = false_positive + noise
|
|
211
|
+
cost_usd = spend_map.get(profile_id, 0.0)
|
|
212
|
+
run_count = runs.get(profile_id, 0)
|
|
213
|
+
duration_seconds = durations.get(profile_id, 0.0)
|
|
214
|
+
known_blocked_catches = known_blocked_caught_runs.get(profile_id, 0)
|
|
215
|
+
profiles_out[profile_id] = {
|
|
216
|
+
"profile_id": profile_id,
|
|
217
|
+
"model": models.get(profile_id, ""),
|
|
218
|
+
"runs": run_count,
|
|
219
|
+
"duration_seconds_total": round(duration_seconds, 3),
|
|
220
|
+
"cost_usd": round(cost_usd, 4),
|
|
221
|
+
"dispositions": dict(sorted(profile_counts.items())),
|
|
222
|
+
"known_disposition_count": known,
|
|
223
|
+
"unknown_disposition_count": unknown,
|
|
224
|
+
"known_clean_pass_runs": known_clean_pass_runs.get(profile_id, 0),
|
|
225
|
+
"blocking_false_positive_runs": blocking_false_positive_runs.get(profile_id, 0),
|
|
226
|
+
"known_blocked_runs": known_blocked_runs.get(profile_id, 0),
|
|
227
|
+
"known_blocked_caught_runs": known_blocked_caught_runs.get(profile_id, 0),
|
|
228
|
+
"known_blocked_missed_runs": known_blocked_missed_runs.get(profile_id, 0),
|
|
229
|
+
"infra_error_runs": infra_error_runs.get(profile_id, 0),
|
|
230
|
+
"audit_input_insufficient_runs": audit_input_insufficient_runs.get(
|
|
231
|
+
profile_id, 0
|
|
232
|
+
),
|
|
233
|
+
"run_statuses": dict(sorted(run_statuses.get(profile_id, {}).items())),
|
|
234
|
+
"review_classes": sorted(review_classes.get(profile_id, set())),
|
|
235
|
+
"context_packs": sorted(context_packs.get(profile_id, set())),
|
|
236
|
+
"useful_review_classes": sorted(
|
|
237
|
+
useful_review_classes.get(profile_id, set())
|
|
238
|
+
),
|
|
239
|
+
"useful_context_packs": sorted(
|
|
240
|
+
useful_context_packs.get(profile_id, set())
|
|
241
|
+
),
|
|
242
|
+
"useful_findings": useful_total,
|
|
243
|
+
"negative_findings": negative_total,
|
|
244
|
+
"precision": _rate(true_positive, true_positive + false_positive),
|
|
245
|
+
"useful_rate": _rate(useful_total, known),
|
|
246
|
+
"noise_rate": _rate(negative_total, known),
|
|
247
|
+
"useful_findings_per_usd": (
|
|
248
|
+
round(useful_total / cost_usd, 4) if cost_usd > 0 else None
|
|
249
|
+
),
|
|
250
|
+
"cost_per_run": (
|
|
251
|
+
round(cost_usd / run_count, 4) if cost_usd > 0 and run_count else None
|
|
252
|
+
),
|
|
253
|
+
"cost_per_useful_finding": (
|
|
254
|
+
round(cost_usd / useful_total, 4) if cost_usd > 0 and useful_total else None
|
|
255
|
+
),
|
|
256
|
+
"cost_per_known_blocked_catch": (
|
|
257
|
+
round(cost_usd / known_blocked_catches, 4)
|
|
258
|
+
if cost_usd > 0 and known_blocked_catches
|
|
259
|
+
else None
|
|
260
|
+
),
|
|
261
|
+
"seconds_per_run": (
|
|
262
|
+
round(duration_seconds / run_count, 3) if run_count else None
|
|
263
|
+
),
|
|
264
|
+
"seconds_per_useful_finding": (
|
|
265
|
+
round(duration_seconds / useful_total, 3)
|
|
266
|
+
if useful_total
|
|
267
|
+
else None
|
|
268
|
+
),
|
|
269
|
+
"seconds_per_known_blocked_catch": (
|
|
270
|
+
round(duration_seconds / known_blocked_catches, 3)
|
|
271
|
+
if known_blocked_catches
|
|
272
|
+
else None
|
|
273
|
+
),
|
|
274
|
+
"event_log": lane_events.get(profile_id, {}),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"mode": "reviewer-metrics",
|
|
279
|
+
"source_report_count": source_count,
|
|
280
|
+
"event_summary_count": event_summary_count,
|
|
281
|
+
"profiles": profiles_out,
|
|
282
|
+
"recommendations": _recommendations(profiles_out),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _recommendations(profiles: Mapping[str, Mapping[str, Any]]) -> list[str]:
|
|
287
|
+
recommendations: list[str] = []
|
|
288
|
+
for profile_id, stats in profiles.items():
|
|
289
|
+
if stats.get("known_disposition_count", 0) == 0:
|
|
290
|
+
recommendations.append(
|
|
291
|
+
f"{profile_id}: collect human dispositions before comparing reviewer accuracy."
|
|
292
|
+
)
|
|
293
|
+
continue
|
|
294
|
+
if (stats.get("useful_rate") or 0) < 0.5:
|
|
295
|
+
recommendations.append(
|
|
296
|
+
f"{profile_id}: low useful-rate; keep informational until prompt or context improves."
|
|
297
|
+
)
|
|
298
|
+
if stats.get("known_blocked_missed_runs", 0):
|
|
299
|
+
recommendations.append(
|
|
300
|
+
f"{profile_id}: missed known-blocked calibration runs; keep informational until catch rate improves."
|
|
301
|
+
)
|
|
302
|
+
if stats.get("audit_input_insufficient_runs", 0):
|
|
303
|
+
recommendations.append(
|
|
304
|
+
f"{profile_id}: audit input was insufficient on some runs; add context packs or larger budgets before promotion."
|
|
305
|
+
)
|
|
306
|
+
if stats.get("cost_usd", 0) and not stats.get("useful_findings", 0):
|
|
307
|
+
recommendations.append(
|
|
308
|
+
f"{profile_id}: spend recorded but no useful findings; pause paid runs or narrow triggers."
|
|
309
|
+
)
|
|
310
|
+
if not recommendations:
|
|
311
|
+
recommendations.append(
|
|
312
|
+
"Use these metrics as calibration evidence, not automatic merge authority."
|
|
313
|
+
)
|
|
314
|
+
return recommendations
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def render_reviewer_metrics_text(report: Mapping[str, Any]) -> str:
|
|
318
|
+
lines = [
|
|
319
|
+
"Code Mower reviewer metrics",
|
|
320
|
+
"",
|
|
321
|
+
"Profiles:",
|
|
322
|
+
]
|
|
323
|
+
profiles = report.get("profiles", {})
|
|
324
|
+
if isinstance(profiles, Mapping) and profiles:
|
|
325
|
+
for profile_id, stats in profiles.items():
|
|
326
|
+
if not isinstance(stats, Mapping):
|
|
327
|
+
continue
|
|
328
|
+
lines.append(
|
|
329
|
+
f"- {profile_id}: useful={stats.get('useful_findings', 0)} "
|
|
330
|
+
f"negative={stats.get('negative_findings', 0)} "
|
|
331
|
+
f"useful_rate={stats.get('useful_rate')} "
|
|
332
|
+
f"cost=${stats.get('cost_usd', 0)} "
|
|
333
|
+
f"sec_per_run={stats.get('seconds_per_run')} "
|
|
334
|
+
f"useful_per_usd={stats.get('useful_findings_per_usd')} "
|
|
335
|
+
f"cost_per_useful={stats.get('cost_per_useful_finding')}"
|
|
336
|
+
)
|
|
337
|
+
event_log = stats.get("event_log", {})
|
|
338
|
+
if isinstance(event_log, Mapping) and event_log:
|
|
339
|
+
lines.append(
|
|
340
|
+
f" events: finished={event_log.get('finished', 0)} "
|
|
341
|
+
f"pass={event_log.get('pass', 0)} blocked={event_log.get('blocked', 0)} "
|
|
342
|
+
f"findings={event_log.get('findings', 0)}"
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
lines.append("- none")
|
|
346
|
+
lines.extend(["", "Recommendations:"])
|
|
347
|
+
for recommendation in report.get("recommendations", []) or []:
|
|
348
|
+
lines.append(f"- {recommendation}")
|
|
349
|
+
return "\n".join(lines) + "\n"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def main(argv: list[str] | None = None) -> int:
|
|
353
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
354
|
+
parser.add_argument("calibration_reports", nargs="*", type=Path)
|
|
355
|
+
parser.add_argument("--spend", type=Path, default=None)
|
|
356
|
+
parser.add_argument(
|
|
357
|
+
"--events",
|
|
358
|
+
nargs="+",
|
|
359
|
+
type=Path,
|
|
360
|
+
default=[],
|
|
361
|
+
help="Optional Code Mower audit event JSONL logs to fold into lane value metrics.",
|
|
362
|
+
)
|
|
363
|
+
parser.add_argument("--json", action="store_true")
|
|
364
|
+
args = parser.parse_args(argv)
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
if not args.calibration_reports and not args.events:
|
|
368
|
+
raise ValueError("pass at least one calibration report or --events log")
|
|
369
|
+
reports = [_load_json(path) for path in args.calibration_reports]
|
|
370
|
+
spend = _load_json(args.spend) if args.spend is not None else None
|
|
371
|
+
event_summaries = [_load_event_summary(path) for path in args.events]
|
|
372
|
+
metrics = build_reviewer_metrics(
|
|
373
|
+
reports,
|
|
374
|
+
spend=spend,
|
|
375
|
+
event_summaries=event_summaries,
|
|
376
|
+
)
|
|
377
|
+
except ValueError as exc:
|
|
378
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
379
|
+
return 1
|
|
380
|
+
|
|
381
|
+
if args.json:
|
|
382
|
+
print(json.dumps(metrics, indent=2, sort_keys=True))
|
|
383
|
+
else:
|
|
384
|
+
print(render_reviewer_metrics_text(metrics), end="")
|
|
385
|
+
return 0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
raise SystemExit(main())
|