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,561 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Summarize Code Mower audit/review telemetry from JSONL event logs."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from collections import Counter, defaultdict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
15
|
+
|
|
16
|
+
if __package__ in {None, ""}:
|
|
17
|
+
module_dir = Path(__file__).resolve().parent
|
|
18
|
+
sys.path.insert(0, str(module_dir.parent))
|
|
19
|
+
if module_dir.name == "code_mower": # pragma: no cover - extracted direct CLI.
|
|
20
|
+
from code_mower import audit_handoff_log
|
|
21
|
+
else:
|
|
22
|
+
from tools import audit_handoff_log
|
|
23
|
+
elif __package__ == "tools":
|
|
24
|
+
from tools import audit_handoff_log
|
|
25
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
26
|
+
from . import audit_handoff_log
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
PASS_VERDICTS = {"completed", "done", "pass", "passed", "success", "succeeded"}
|
|
30
|
+
BLOCKED_VERDICTS = {"block", "blocked", "fail", "failed", "failure"}
|
|
31
|
+
VERDICT_ARTIFACT_SCHEMA = "code_mower.auditVerdictArtifact.v1"
|
|
32
|
+
BENCHMARK_EVENT_SCHEMA = "code_mower.benchmarkEvent.v1"
|
|
33
|
+
VERDICT_ARTIFACT_DIR_ENV = "CODE_MOWER_VERDICT_ARTIFACT_DIR"
|
|
34
|
+
DEFAULT_VERDICT_ARTIFACT_DIR = Path.home() / ".cache" / "code-mower-audits" / "verdicts"
|
|
35
|
+
SEVERITY_RE = re.compile(r"\b(P[0-3])=(\d+)\b")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_verdict_artifact_dir() -> Path:
|
|
39
|
+
configured = os.environ.get(VERDICT_ARTIFACT_DIR_ENV, "").strip()
|
|
40
|
+
if configured:
|
|
41
|
+
return Path(configured).expanduser()
|
|
42
|
+
return DEFAULT_VERDICT_ARTIFACT_DIR
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _normalize_repo_slug(value: str) -> str:
|
|
46
|
+
text = value.strip().strip("/")
|
|
47
|
+
parts = [part for part in text.split("/") if part]
|
|
48
|
+
if len(parts) != 2:
|
|
49
|
+
return text
|
|
50
|
+
return f"{parts[0]}/{parts[1]}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _repo_slug_from_artifact_path(path: Path) -> str:
|
|
54
|
+
for parent in [path.parent, *path.parents]:
|
|
55
|
+
name = parent.name
|
|
56
|
+
if "__" in name:
|
|
57
|
+
owner, repo = name.split("__", 1)
|
|
58
|
+
if owner and repo:
|
|
59
|
+
return f"{owner}/{repo}"
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _artifact_pr_from_path(path: Path) -> int | None:
|
|
64
|
+
for parent in [path.parent, *path.parents]:
|
|
65
|
+
match = re.fullmatch(r"pr-(\d+)", parent.name)
|
|
66
|
+
if match:
|
|
67
|
+
return int(match.group(1))
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _lane_provider(lane_id: str) -> str:
|
|
72
|
+
if lane_id.endswith("-audit"):
|
|
73
|
+
return lane_id.removesuffix("-audit")
|
|
74
|
+
return lane_id or "unknown"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _severity_counts(comment_body: str) -> dict[str, int]:
|
|
78
|
+
counts = {"p0": 0, "p1": 0, "p2": 0, "p3": 0}
|
|
79
|
+
for severity, value in SEVERITY_RE.findall(comment_body):
|
|
80
|
+
counts[severity.lower()] += int(value)
|
|
81
|
+
return counts
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _coerce_severity_counts(value: Any) -> dict[str, int] | None:
|
|
85
|
+
if not isinstance(value, Mapping):
|
|
86
|
+
return None
|
|
87
|
+
counts = {"p0": 0, "p1": 0, "p2": 0, "p3": 0}
|
|
88
|
+
found = False
|
|
89
|
+
for key in counts:
|
|
90
|
+
raw = value.get(key) if key in value else value.get(key.upper())
|
|
91
|
+
if raw is None:
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
counts[key] = int(raw)
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
return None
|
|
97
|
+
found = True
|
|
98
|
+
return counts if found else None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _artifact_severity_counts(payload: Mapping[str, Any]) -> tuple[dict[str, int], str]:
|
|
102
|
+
structured_counts = _coerce_severity_counts(payload.get("severity_counts"))
|
|
103
|
+
if structured_counts is not None:
|
|
104
|
+
return structured_counts, "structured"
|
|
105
|
+
finding_count = payload.get("finding_count")
|
|
106
|
+
if isinstance(finding_count, int):
|
|
107
|
+
return {"p0": 0, "p1": 0, "p2": 0, "p3": 0}, "finding_count_only"
|
|
108
|
+
comment_counts = _severity_counts(str(payload.get("comment_body") or ""))
|
|
109
|
+
if any(comment_counts.values()):
|
|
110
|
+
return comment_counts, "severity_summary"
|
|
111
|
+
return comment_counts, "unavailable"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _normal_artifact_verdict(value: Any) -> str:
|
|
115
|
+
verdict = str(value or "").strip().lower()
|
|
116
|
+
if verdict in PASS_VERDICTS:
|
|
117
|
+
return "pass"
|
|
118
|
+
if verdict in BLOCKED_VERDICTS:
|
|
119
|
+
return "blocked"
|
|
120
|
+
return verdict or "unknown"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _verdict_event_id(payload: Mapping[str, Any], path: Path) -> str:
|
|
124
|
+
# Keep event IDs stable without encoding private git refs. Git refs are only
|
|
125
|
+
# exported when the operator explicitly passes --include-git-ref.
|
|
126
|
+
seed = "|".join(
|
|
127
|
+
[
|
|
128
|
+
str(payload.get("repo") or _repo_slug_from_artifact_path(path)),
|
|
129
|
+
str(payload.get("pr_number") or _artifact_pr_from_path(path) or ""),
|
|
130
|
+
str(payload.get("lane_id") or ""),
|
|
131
|
+
str(payload.get("verdict") or ""),
|
|
132
|
+
str(payload.get("created_at") or ""),
|
|
133
|
+
path.name,
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:20]
|
|
137
|
+
return f"verdict-artifact-{digest}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def reviewer_run_event_from_verdict_artifact(
|
|
141
|
+
payload: Mapping[str, Any],
|
|
142
|
+
*,
|
|
143
|
+
path: Path,
|
|
144
|
+
include_git_ref: bool = False,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
if payload.get("schema") != VERDICT_ARTIFACT_SCHEMA:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"unsupported verdict artifact schema {payload.get('schema')!r}; "
|
|
149
|
+
f"expected {VERDICT_ARTIFACT_SCHEMA!r}"
|
|
150
|
+
)
|
|
151
|
+
lane_id = str(payload.get("lane_id") or "unknown")
|
|
152
|
+
repo_slug = _normalize_repo_slug(
|
|
153
|
+
str(payload.get("repo") or _repo_slug_from_artifact_path(path))
|
|
154
|
+
)
|
|
155
|
+
pr_number = payload.get("pr_number")
|
|
156
|
+
if pr_number is None:
|
|
157
|
+
pr_number = _artifact_pr_from_path(path)
|
|
158
|
+
try:
|
|
159
|
+
pr_number_value: int | None = int(pr_number) if pr_number is not None else None
|
|
160
|
+
except (TypeError, ValueError):
|
|
161
|
+
pr_number_value = None
|
|
162
|
+
severities, severity_source = _artifact_severity_counts(payload)
|
|
163
|
+
finding_count_raw = payload.get("finding_count")
|
|
164
|
+
finding_count = (
|
|
165
|
+
int(finding_count_raw)
|
|
166
|
+
if isinstance(finding_count_raw, int)
|
|
167
|
+
else sum(severities.values())
|
|
168
|
+
)
|
|
169
|
+
status = _normal_artifact_verdict(payload.get("verdict"))
|
|
170
|
+
dimensions: dict[str, Any] = {
|
|
171
|
+
"lane_id": lane_id,
|
|
172
|
+
"pr_number": pr_number_value,
|
|
173
|
+
"artifact_source": "local-verdict-cache",
|
|
174
|
+
"git_ref_included": include_git_ref,
|
|
175
|
+
"severity_count_source": severity_source,
|
|
176
|
+
}
|
|
177
|
+
if include_git_ref:
|
|
178
|
+
head = str(payload.get("head_sha_end") or payload.get("head_sha_start") or "")
|
|
179
|
+
dimensions["head_sha"] = head
|
|
180
|
+
dimensions["head_sha_short"] = head[:12]
|
|
181
|
+
return {
|
|
182
|
+
"schema": BENCHMARK_EVENT_SCHEMA,
|
|
183
|
+
"event_type": "reviewer_run",
|
|
184
|
+
"event_id": _verdict_event_id(payload, path),
|
|
185
|
+
"created_at": str(payload.get("created_at") or ""),
|
|
186
|
+
"repo_slug": repo_slug,
|
|
187
|
+
"source": "verdict-artifact-export",
|
|
188
|
+
"provider": _lane_provider(lane_id),
|
|
189
|
+
"lens": "base",
|
|
190
|
+
"status": status,
|
|
191
|
+
"metrics": {
|
|
192
|
+
"finding_count": finding_count,
|
|
193
|
+
"p0_count": severities["p0"],
|
|
194
|
+
"p1_count": severities["p1"],
|
|
195
|
+
"p2_count": severities["p2"],
|
|
196
|
+
"p3_count": severities["p3"],
|
|
197
|
+
},
|
|
198
|
+
"dimensions": dimensions,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _iter_verdict_artifact_paths(root: Path) -> list[Path]:
|
|
203
|
+
if not root.exists():
|
|
204
|
+
return []
|
|
205
|
+
if root.is_file():
|
|
206
|
+
return [root]
|
|
207
|
+
return sorted(root.glob("**/*.json"))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def export_reviewer_run_events_from_verdicts(
|
|
211
|
+
root: Path,
|
|
212
|
+
*,
|
|
213
|
+
repo: str = "",
|
|
214
|
+
limit: int = 0,
|
|
215
|
+
include_git_ref: bool = False,
|
|
216
|
+
) -> list[dict[str, Any]]:
|
|
217
|
+
repo_filter = _normalize_repo_slug(repo) if repo else ""
|
|
218
|
+
events: list[dict[str, Any]] = []
|
|
219
|
+
for path in _iter_verdict_artifact_paths(root.expanduser()):
|
|
220
|
+
try:
|
|
221
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
222
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
223
|
+
raise ValueError(f"{path}: invalid verdict artifact: {exc}") from exc
|
|
224
|
+
if not isinstance(payload, dict):
|
|
225
|
+
raise ValueError(f"{path}: verdict artifact must be a JSON object")
|
|
226
|
+
if payload.get("schema") != VERDICT_ARTIFACT_SCHEMA:
|
|
227
|
+
continue
|
|
228
|
+
artifact_repo = _normalize_repo_slug(
|
|
229
|
+
str(payload.get("repo") or _repo_slug_from_artifact_path(path))
|
|
230
|
+
)
|
|
231
|
+
if repo_filter and artifact_repo != repo_filter:
|
|
232
|
+
continue
|
|
233
|
+
events.append(
|
|
234
|
+
reviewer_run_event_from_verdict_artifact(
|
|
235
|
+
payload,
|
|
236
|
+
path=path,
|
|
237
|
+
include_git_ref=include_git_ref,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
events.sort(
|
|
241
|
+
key=lambda event: (str(event.get("created_at") or ""), str(event.get("event_id") or "")),
|
|
242
|
+
reverse=True,
|
|
243
|
+
)
|
|
244
|
+
return events[:limit] if limit else events
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def load_jsonl_events(path: Path) -> list[dict[str, Any]]:
|
|
248
|
+
events: list[dict[str, Any]] = []
|
|
249
|
+
if not path.exists():
|
|
250
|
+
return events
|
|
251
|
+
for line_no, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
252
|
+
if not raw.strip():
|
|
253
|
+
continue
|
|
254
|
+
try:
|
|
255
|
+
payload = json.loads(raw)
|
|
256
|
+
except json.JSONDecodeError as exc:
|
|
257
|
+
raise ValueError(f"{path}:{line_no}: invalid JSON: {exc}") from exc
|
|
258
|
+
if not isinstance(payload, dict):
|
|
259
|
+
raise ValueError(f"{path}:{line_no}: event must be a JSON object")
|
|
260
|
+
events.append(payload)
|
|
261
|
+
return events
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _event_lane(event: Mapping[str, Any]) -> str:
|
|
265
|
+
lane = event.get("lane")
|
|
266
|
+
if lane:
|
|
267
|
+
return str(lane)
|
|
268
|
+
dimensions = event.get("dimensions")
|
|
269
|
+
if isinstance(dimensions, Mapping) and dimensions.get("lane_id"):
|
|
270
|
+
return str(dimensions["lane_id"])
|
|
271
|
+
provider = event.get("provider")
|
|
272
|
+
if provider:
|
|
273
|
+
lens = str(event.get("lens") or "base")
|
|
274
|
+
return f"{provider}:{lens}" if lens else str(provider)
|
|
275
|
+
lock = event.get("lock")
|
|
276
|
+
if isinstance(lock, Mapping) and lock.get("lane"):
|
|
277
|
+
return str(lock["lane"])
|
|
278
|
+
active = event.get("active")
|
|
279
|
+
if isinstance(active, Mapping) and active.get("lane"):
|
|
280
|
+
return str(active["lane"])
|
|
281
|
+
return "unknown"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _event_repo(event: Mapping[str, Any]) -> str:
|
|
285
|
+
repo_slug = event.get("repo_slug")
|
|
286
|
+
if repo_slug:
|
|
287
|
+
return str(repo_slug)
|
|
288
|
+
repo = event.get("repo")
|
|
289
|
+
if repo:
|
|
290
|
+
return str(repo)
|
|
291
|
+
lock = event.get("lock")
|
|
292
|
+
if isinstance(lock, Mapping) and lock.get("repo"):
|
|
293
|
+
return str(lock["repo"])
|
|
294
|
+
return "unknown"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _event_pr(event: Mapping[str, Any]) -> str:
|
|
298
|
+
pr = event.get("pr")
|
|
299
|
+
if pr is not None:
|
|
300
|
+
return str(pr)
|
|
301
|
+
dimensions = event.get("dimensions")
|
|
302
|
+
if isinstance(dimensions, Mapping) and dimensions.get("pr_number") is not None:
|
|
303
|
+
return str(dimensions["pr_number"])
|
|
304
|
+
lock = event.get("lock")
|
|
305
|
+
if isinstance(lock, Mapping) and lock.get("pr") is not None:
|
|
306
|
+
return str(lock["pr"])
|
|
307
|
+
return "unknown"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _event_head(event: Mapping[str, Any]) -> str:
|
|
311
|
+
head = event.get("head")
|
|
312
|
+
if head:
|
|
313
|
+
return str(head)
|
|
314
|
+
lock = event.get("lock")
|
|
315
|
+
if isinstance(lock, Mapping) and lock.get("head"):
|
|
316
|
+
return str(lock["head"])
|
|
317
|
+
return "unknown"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _normal_verdict(value: Any) -> str:
|
|
321
|
+
text = str(value or "").strip().lower()
|
|
322
|
+
if text in PASS_VERDICTS:
|
|
323
|
+
return "pass"
|
|
324
|
+
if text in BLOCKED_VERDICTS:
|
|
325
|
+
return "blocked"
|
|
326
|
+
return text or "unknown"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _event_outcome(event: Mapping[str, Any]) -> str:
|
|
330
|
+
if event.get("verdict") is not None:
|
|
331
|
+
return _normal_verdict(event.get("verdict"))
|
|
332
|
+
status = str(event.get("status") or "").strip().lower()
|
|
333
|
+
if status in PASS_VERDICTS or status in BLOCKED_VERDICTS:
|
|
334
|
+
return _normal_verdict(status)
|
|
335
|
+
return "unknown"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _finding_count(event: Mapping[str, Any]) -> int:
|
|
339
|
+
metrics = event.get("metrics")
|
|
340
|
+
if isinstance(metrics, Mapping):
|
|
341
|
+
for key in ("finding_count", "findings_count", "findings"):
|
|
342
|
+
value = metrics.get(key)
|
|
343
|
+
if isinstance(value, int):
|
|
344
|
+
return value
|
|
345
|
+
if isinstance(value, list):
|
|
346
|
+
return len(value)
|
|
347
|
+
for key in ("finding_count", "findings_count", "findings"):
|
|
348
|
+
value = event.get(key)
|
|
349
|
+
if isinstance(value, int):
|
|
350
|
+
return value
|
|
351
|
+
if isinstance(value, list):
|
|
352
|
+
return len(value)
|
|
353
|
+
return 0
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def summarize_events(events: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
|
|
357
|
+
event_list = list(events)
|
|
358
|
+
by_event = Counter(
|
|
359
|
+
str(event.get("event") or event.get("event_type") or "unknown")
|
|
360
|
+
for event in event_list
|
|
361
|
+
)
|
|
362
|
+
by_lane = Counter(_event_lane(event) for event in event_list)
|
|
363
|
+
by_verdict = Counter(_event_outcome(event) for event in event_list)
|
|
364
|
+
by_verdict.pop("unknown", None)
|
|
365
|
+
|
|
366
|
+
lane_stats: dict[str, dict[str, Any]] = {}
|
|
367
|
+
lane_heads: dict[str, set[str]] = defaultdict(set)
|
|
368
|
+
lane_repos: dict[str, set[str]] = defaultdict(set)
|
|
369
|
+
lane_prs: dict[str, set[str]] = defaultdict(set)
|
|
370
|
+
lane_repo_prs: dict[str, set[str]] = defaultdict(set)
|
|
371
|
+
authoring_tools = Counter()
|
|
372
|
+
|
|
373
|
+
for event in event_list:
|
|
374
|
+
lane = _event_lane(event)
|
|
375
|
+
repo = _event_repo(event)
|
|
376
|
+
pr = _event_pr(event)
|
|
377
|
+
stats = lane_stats.setdefault(
|
|
378
|
+
lane,
|
|
379
|
+
{
|
|
380
|
+
"events": 0,
|
|
381
|
+
"started": 0,
|
|
382
|
+
"finished": 0,
|
|
383
|
+
"pass": 0,
|
|
384
|
+
"blocked": 0,
|
|
385
|
+
"failed": 0,
|
|
386
|
+
"findings": 0,
|
|
387
|
+
},
|
|
388
|
+
)
|
|
389
|
+
stats["events"] += 1
|
|
390
|
+
event_name = str(event.get("event") or event.get("event_type") or "unknown")
|
|
391
|
+
if event_name == "started":
|
|
392
|
+
stats["started"] += 1
|
|
393
|
+
if event_name in {"finished", "reviewer_run", "calibration_run"}:
|
|
394
|
+
stats["finished"] += 1
|
|
395
|
+
verdict = _event_outcome(event)
|
|
396
|
+
if verdict in {"pass", "blocked"}:
|
|
397
|
+
stats[verdict] += 1
|
|
398
|
+
if str(event.get("status", "")).lower() == "failed":
|
|
399
|
+
stats["failed"] += 1
|
|
400
|
+
stats["findings"] += _finding_count(event)
|
|
401
|
+
lane_heads[lane].add(_event_head(event))
|
|
402
|
+
lane_repos[lane].add(repo)
|
|
403
|
+
lane_prs[lane].add(pr)
|
|
404
|
+
if repo != "unknown" or pr != "unknown":
|
|
405
|
+
lane_repo_prs[lane].add(f"{repo}#{pr}")
|
|
406
|
+
|
|
407
|
+
authoring_tool = (
|
|
408
|
+
event.get("authoring_tool")
|
|
409
|
+
or event.get("authoringTool")
|
|
410
|
+
or event.get("tool")
|
|
411
|
+
)
|
|
412
|
+
if authoring_tool:
|
|
413
|
+
authoring_tools[str(authoring_tool)] += 1
|
|
414
|
+
|
|
415
|
+
for lane, stats in lane_stats.items():
|
|
416
|
+
heads = sorted(item for item in lane_heads[lane] if item != "unknown")
|
|
417
|
+
repos = sorted(item for item in lane_repos[lane] if item != "unknown")
|
|
418
|
+
prs = sorted(item for item in lane_prs[lane] if item != "unknown")
|
|
419
|
+
repo_prs = sorted(item for item in lane_repo_prs[lane] if item != "unknown#unknown")
|
|
420
|
+
stats["heads"] = heads
|
|
421
|
+
stats["repositories"] = repos
|
|
422
|
+
stats["pull_requests"] = prs
|
|
423
|
+
stats["repo_pull_requests"] = repo_prs
|
|
424
|
+
stats["observed_pr_count"] = len(repo_prs)
|
|
425
|
+
finished = stats["finished"]
|
|
426
|
+
stats["pass_rate"] = round(stats["pass"] / finished, 4) if finished else None
|
|
427
|
+
stats["blocked_rate"] = (
|
|
428
|
+
round(stats["blocked"] / finished, 4) if finished else None
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
"mode": "telemetry-summary",
|
|
433
|
+
"event_count": len(event_list),
|
|
434
|
+
"events_by_type": dict(sorted(by_event.items())),
|
|
435
|
+
"events_by_lane": dict(sorted(by_lane.items())),
|
|
436
|
+
"verdicts": dict(sorted(by_verdict.items())),
|
|
437
|
+
"lanes": dict(sorted(lane_stats.items())),
|
|
438
|
+
"authoring_intelligence": {
|
|
439
|
+
"observed_authoring_tools": dict(sorted(authoring_tools.items())),
|
|
440
|
+
"caveat": (
|
|
441
|
+
"Observational telemetry can compare findings density and "
|
|
442
|
+
"head-to-head catch differential, but it is not causal proof "
|
|
443
|
+
"of which AI writes better code."
|
|
444
|
+
),
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def render_summary_text(summary: Mapping[str, Any]) -> str:
|
|
450
|
+
lines = [
|
|
451
|
+
"Code Mower telemetry summary",
|
|
452
|
+
f"Events: {summary.get('event_count', 0)}",
|
|
453
|
+
"",
|
|
454
|
+
"Lanes:",
|
|
455
|
+
]
|
|
456
|
+
lanes = summary.get("lanes", {})
|
|
457
|
+
if isinstance(lanes, Mapping) and lanes:
|
|
458
|
+
for lane, stats in lanes.items():
|
|
459
|
+
if not isinstance(stats, Mapping):
|
|
460
|
+
continue
|
|
461
|
+
lines.append(
|
|
462
|
+
f"- {lane}: finished={stats.get('finished', 0)} "
|
|
463
|
+
f"pass={stats.get('pass', 0)} blocked={stats.get('blocked', 0)} "
|
|
464
|
+
f"findings={stats.get('findings', 0)}"
|
|
465
|
+
)
|
|
466
|
+
else:
|
|
467
|
+
lines.append("- none")
|
|
468
|
+
caveat = summary.get("authoring_intelligence", {})
|
|
469
|
+
if isinstance(caveat, Mapping):
|
|
470
|
+
lines.extend(["", f"Caveat: {caveat.get('caveat', '')}"])
|
|
471
|
+
return "\n".join(lines) + "\n"
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
475
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
476
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
477
|
+
summarize = subparsers.add_parser("summarize")
|
|
478
|
+
summarize.add_argument(
|
|
479
|
+
"events",
|
|
480
|
+
nargs="?",
|
|
481
|
+
type=Path,
|
|
482
|
+
default=audit_handoff_log.events_path(),
|
|
483
|
+
)
|
|
484
|
+
summarize.add_argument("--json", action="store_true")
|
|
485
|
+
export_verdicts = subparsers.add_parser(
|
|
486
|
+
"export-verdict-events",
|
|
487
|
+
help="Export saved audit verdict artifacts as metadata-only reviewer_run JSONL events.",
|
|
488
|
+
)
|
|
489
|
+
export_verdicts.add_argument(
|
|
490
|
+
"verdicts",
|
|
491
|
+
nargs="?",
|
|
492
|
+
type=Path,
|
|
493
|
+
default=default_verdict_artifact_dir(),
|
|
494
|
+
help="Verdict artifact file or directory.",
|
|
495
|
+
)
|
|
496
|
+
export_verdicts.add_argument("--repo", default="", help="Optional owner/repo filter.")
|
|
497
|
+
export_verdicts.add_argument(
|
|
498
|
+
"--limit",
|
|
499
|
+
type=int,
|
|
500
|
+
default=0,
|
|
501
|
+
help="Maximum events to export; 0 means no limit.",
|
|
502
|
+
)
|
|
503
|
+
export_verdicts.add_argument(
|
|
504
|
+
"--include-git-ref",
|
|
505
|
+
action="store_true",
|
|
506
|
+
help="Include head SHA metadata. Off by default for privacy.",
|
|
507
|
+
)
|
|
508
|
+
export_verdicts.add_argument(
|
|
509
|
+
"--output",
|
|
510
|
+
type=Path,
|
|
511
|
+
help="Write JSONL events to this path instead of stdout.",
|
|
512
|
+
)
|
|
513
|
+
args = parser.parse_args(argv)
|
|
514
|
+
|
|
515
|
+
if args.command == "summarize":
|
|
516
|
+
try:
|
|
517
|
+
summary = summarize_events(load_jsonl_events(args.events))
|
|
518
|
+
except (OSError, ValueError) as exc:
|
|
519
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
520
|
+
return 1
|
|
521
|
+
|
|
522
|
+
if args.json:
|
|
523
|
+
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
524
|
+
else:
|
|
525
|
+
print(render_summary_text(summary), end="")
|
|
526
|
+
return 0
|
|
527
|
+
|
|
528
|
+
if args.command == "export-verdict-events":
|
|
529
|
+
try:
|
|
530
|
+
events = export_reviewer_run_events_from_verdicts(
|
|
531
|
+
args.verdicts,
|
|
532
|
+
repo=args.repo,
|
|
533
|
+
limit=args.limit,
|
|
534
|
+
include_git_ref=args.include_git_ref,
|
|
535
|
+
)
|
|
536
|
+
except (OSError, ValueError) as exc:
|
|
537
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
538
|
+
return 1
|
|
539
|
+
text = "".join(json.dumps(event, sort_keys=True) + "\n" for event in events)
|
|
540
|
+
if not events:
|
|
541
|
+
print(
|
|
542
|
+
"warning: no verdict artifacts matched; no reviewer_run events exported",
|
|
543
|
+
file=sys.stderr,
|
|
544
|
+
)
|
|
545
|
+
return 0
|
|
546
|
+
if args.output:
|
|
547
|
+
try:
|
|
548
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
549
|
+
args.output.write_text(text, encoding="utf-8")
|
|
550
|
+
except OSError as exc:
|
|
551
|
+
print(f"error: failed to write {args.output}: {exc}", file=sys.stderr)
|
|
552
|
+
return 1
|
|
553
|
+
else:
|
|
554
|
+
print(text, end="")
|
|
555
|
+
return 0
|
|
556
|
+
|
|
557
|
+
return 0
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
if __name__ == "__main__":
|
|
561
|
+
raise SystemExit(main())
|