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,199 @@
|
|
|
1
|
+
"""Context-pack input materialization for calibration runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
from .commands import repo_path_for_item
|
|
11
|
+
from .identity import safe_slug
|
|
12
|
+
|
|
13
|
+
if __package__ == "tools.calibration": # pragma: no cover - legacy product layout.
|
|
14
|
+
from tools import code_mower_context_packs
|
|
15
|
+
else: # pragma: no cover - import shape depends on package layout.
|
|
16
|
+
from .. import code_mower_context_packs
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CONTEXT_PACK_CLI_LANES = {"antigravity-cli", "gemini-cli", "hermes-cli"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def repo_roots_from_path_map(repo_path_map: Mapping[str, str]) -> dict[str, Path]:
|
|
23
|
+
roots: dict[str, Path] = {}
|
|
24
|
+
for selector, path_text in repo_path_map.items():
|
|
25
|
+
repo = re.split(r"[#@]", str(selector), maxsplit=1)[0]
|
|
26
|
+
if "/" in repo and str(path_text).strip():
|
|
27
|
+
roots.setdefault(repo, Path(path_text).expanduser())
|
|
28
|
+
return roots
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def changed_files_from_checkout(repo_path: Path, base_ref: str) -> list[dict[str, str]]:
|
|
32
|
+
completed = subprocess.run(
|
|
33
|
+
["git", "diff", "--name-only", f"{base_ref}...HEAD"],
|
|
34
|
+
cwd=repo_path,
|
|
35
|
+
text=True,
|
|
36
|
+
stdout=subprocess.PIPE,
|
|
37
|
+
stderr=subprocess.PIPE,
|
|
38
|
+
check=False,
|
|
39
|
+
)
|
|
40
|
+
if completed.returncode != 0:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"unable to list changed files for context packs with "
|
|
43
|
+
f"{base_ref}...HEAD in {repo_path}: {completed.stderr.strip()}"
|
|
44
|
+
)
|
|
45
|
+
return [
|
|
46
|
+
{"filename": line.strip()}
|
|
47
|
+
for line in completed.stdout.splitlines()
|
|
48
|
+
if line.strip()
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def selected_context_pack_manifest(
|
|
53
|
+
*,
|
|
54
|
+
context_pack_manifest: Mapping[str, Any],
|
|
55
|
+
item: Mapping[str, Any],
|
|
56
|
+
repo_path: Path,
|
|
57
|
+
) -> dict[str, Any] | None:
|
|
58
|
+
selected_ids = {
|
|
59
|
+
str(pack_id).strip()
|
|
60
|
+
for pack_id in item.get("context_packs", []) or []
|
|
61
|
+
if str(pack_id).strip()
|
|
62
|
+
}
|
|
63
|
+
if not selected_ids:
|
|
64
|
+
return None
|
|
65
|
+
pack_values = context_pack_manifest.get("packs")
|
|
66
|
+
if not isinstance(pack_values, list):
|
|
67
|
+
raise ValueError("context pack manifest must include a packs list")
|
|
68
|
+
packs_by_id = {
|
|
69
|
+
str(pack.get("id") or "").strip(): pack
|
|
70
|
+
for pack in pack_values
|
|
71
|
+
if isinstance(pack, Mapping)
|
|
72
|
+
}
|
|
73
|
+
missing = sorted(pack_id for pack_id in selected_ids if pack_id not in packs_by_id)
|
|
74
|
+
if missing:
|
|
75
|
+
raise ValueError(f"context pack manifest is missing pack id(s): {', '.join(missing)}")
|
|
76
|
+
base_ref = str(item.get("base_ref") or "origin/main")
|
|
77
|
+
return {
|
|
78
|
+
"repo": str(item.get("repo") or context_pack_manifest.get("repo") or ""),
|
|
79
|
+
"pr_number": item.get("pr_number"),
|
|
80
|
+
"head_sha": str(item.get("head_sha") or context_pack_manifest.get("head_sha") or ""),
|
|
81
|
+
"changed_files": changed_files_from_checkout(repo_path, base_ref),
|
|
82
|
+
"packs": [packs_by_id[pack_id] for pack_id in sorted(selected_ids)],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def render_materialized_context_pack_prompt_text(report: Mapping[str, Any]) -> str:
|
|
87
|
+
lines: list[str] = [
|
|
88
|
+
"Code Mower selected context packs",
|
|
89
|
+
f"Manifest: {report.get('manifest_path', '')}",
|
|
90
|
+
"",
|
|
91
|
+
]
|
|
92
|
+
for pack in report.get("packs", []) or []:
|
|
93
|
+
if not isinstance(pack, Mapping):
|
|
94
|
+
continue
|
|
95
|
+
pack_id = str(pack.get("id") or "context-pack")
|
|
96
|
+
reason = str(pack.get("reason") or "").strip()
|
|
97
|
+
lines.append(f"## Context Pack: {pack_id}")
|
|
98
|
+
if reason:
|
|
99
|
+
lines.append(f"Reason: {reason}")
|
|
100
|
+
files = pack.get("files", [])
|
|
101
|
+
if not isinstance(files, list) or not files:
|
|
102
|
+
lines.append("(no files materialized)")
|
|
103
|
+
lines.append("")
|
|
104
|
+
continue
|
|
105
|
+
for file_entry in files:
|
|
106
|
+
if not isinstance(file_entry, Mapping):
|
|
107
|
+
continue
|
|
108
|
+
path = str(file_entry.get("path") or "")
|
|
109
|
+
repo = str(file_entry.get("repo") or report.get("repo") or "")
|
|
110
|
+
if file_entry.get("exists") is False:
|
|
111
|
+
reason_text = str(file_entry.get("reason") or "missing")
|
|
112
|
+
lines.append(f"### {path}")
|
|
113
|
+
if repo:
|
|
114
|
+
lines.append(f"Repository: {repo}")
|
|
115
|
+
lines.append(f"[not included: {reason_text}]")
|
|
116
|
+
lines.append("")
|
|
117
|
+
continue
|
|
118
|
+
artifact_path = Path(str(file_entry.get("artifact_path") or ""))
|
|
119
|
+
try:
|
|
120
|
+
content = artifact_path.read_text(encoding="utf-8", errors="replace")
|
|
121
|
+
except OSError as exc:
|
|
122
|
+
content = f"[unable to read context artifact: {exc}]"
|
|
123
|
+
lines.append(f"### {path}")
|
|
124
|
+
if repo:
|
|
125
|
+
lines.append(f"Repository: {repo}")
|
|
126
|
+
if file_entry.get("truncated"):
|
|
127
|
+
lines.append(
|
|
128
|
+
"[truncated to "
|
|
129
|
+
f"{file_entry.get('bytes_written')} of "
|
|
130
|
+
f"{file_entry.get('source_bytes')} bytes]"
|
|
131
|
+
)
|
|
132
|
+
lines.append("```")
|
|
133
|
+
lines.append(content.rstrip())
|
|
134
|
+
lines.append("```")
|
|
135
|
+
lines.append("")
|
|
136
|
+
warnings = [
|
|
137
|
+
str(warning)
|
|
138
|
+
for warning in report.get("warnings", []) or []
|
|
139
|
+
if str(warning).strip()
|
|
140
|
+
]
|
|
141
|
+
if warnings:
|
|
142
|
+
lines.append("## Context Pack Warnings")
|
|
143
|
+
lines.extend(f"- {warning}" for warning in warnings)
|
|
144
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def context_pack_file_for_command(
|
|
148
|
+
*,
|
|
149
|
+
item: Mapping[str, Any],
|
|
150
|
+
lane_id: str,
|
|
151
|
+
result_dir: Path,
|
|
152
|
+
repo_path_map: Mapping[str, str],
|
|
153
|
+
context_pack_manifest: Mapping[str, Any] | None,
|
|
154
|
+
context_pack_output_dir: Path,
|
|
155
|
+
require_context_pack_files: bool,
|
|
156
|
+
) -> Path | None:
|
|
157
|
+
if lane_id not in CONTEXT_PACK_CLI_LANES or context_pack_manifest is None:
|
|
158
|
+
return None
|
|
159
|
+
selected_ids = [
|
|
160
|
+
str(pack_id).strip()
|
|
161
|
+
for pack_id in item.get("context_packs", []) or []
|
|
162
|
+
if str(pack_id).strip()
|
|
163
|
+
]
|
|
164
|
+
if not selected_ids:
|
|
165
|
+
return None
|
|
166
|
+
repo_path_text = repo_path_for_item(item, repo_path_map)
|
|
167
|
+
if not repo_path_text:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
"context-pack calibration needs --repo-path-map for "
|
|
170
|
+
f"{item.get('repo')}#{item.get('pr_number')}"
|
|
171
|
+
)
|
|
172
|
+
repo_path = Path(repo_path_text).expanduser().resolve()
|
|
173
|
+
if not repo_path.is_dir():
|
|
174
|
+
raise ValueError(f"context-pack repo path is not a directory: {repo_path}")
|
|
175
|
+
manifest = selected_context_pack_manifest(
|
|
176
|
+
context_pack_manifest=context_pack_manifest,
|
|
177
|
+
item=item,
|
|
178
|
+
repo_path=repo_path,
|
|
179
|
+
)
|
|
180
|
+
if manifest is None:
|
|
181
|
+
return None
|
|
182
|
+
plan = code_mower_context_packs.build_context_pack_plan(manifest)
|
|
183
|
+
output_dir = context_pack_output_dir / safe_slug(
|
|
184
|
+
str(item.get("calibration_run_id") or result_dir.parent.name),
|
|
185
|
+
"run",
|
|
186
|
+
) / safe_slug(lane_id, "lane")
|
|
187
|
+
report = code_mower_context_packs.materialize_context_pack_plan(
|
|
188
|
+
plan,
|
|
189
|
+
repo_root=repo_path,
|
|
190
|
+
output_dir=output_dir,
|
|
191
|
+
require_files=require_context_pack_files,
|
|
192
|
+
repo_roots=repo_roots_from_path_map(repo_path_map),
|
|
193
|
+
)
|
|
194
|
+
context_text_path = result_dir / "context-pack.txt"
|
|
195
|
+
context_text_path.write_text(
|
|
196
|
+
render_materialized_context_pack_prompt_text(report),
|
|
197
|
+
encoding="utf-8",
|
|
198
|
+
)
|
|
199
|
+
return context_text_path
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Calibration corpus parsing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
from .truth import normalize_truth
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_json_object(path: Path) -> Mapping[str, Any]:
|
|
13
|
+
try:
|
|
14
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
15
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
16
|
+
raise ValueError(f"unable to read {path}: {exc}") from exc
|
|
17
|
+
if not isinstance(payload, Mapping):
|
|
18
|
+
raise ValueError(f"{path} must contain a JSON object")
|
|
19
|
+
return payload
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_int(value: Any, *, field: str) -> int:
|
|
23
|
+
if isinstance(value, bool):
|
|
24
|
+
raise ValueError(f"{field} must be an integer")
|
|
25
|
+
if isinstance(value, int):
|
|
26
|
+
return value
|
|
27
|
+
if isinstance(value, str) and value.strip().isdigit():
|
|
28
|
+
return int(value.strip())
|
|
29
|
+
raise ValueError(f"{field} must be an integer")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_corpus(path: Path) -> dict[str, Any]:
|
|
33
|
+
payload = dict(load_json_object(path))
|
|
34
|
+
if payload.get("version", 1) not in {1, "1"}:
|
|
35
|
+
raise ValueError("calibration corpus version must be 1")
|
|
36
|
+
raw_items = payload.get("corpus", payload.get("pull_requests"))
|
|
37
|
+
if not isinstance(raw_items, list) or not raw_items:
|
|
38
|
+
raise ValueError("calibration corpus must include a non-empty corpus list")
|
|
39
|
+
|
|
40
|
+
items: list[dict[str, Any]] = []
|
|
41
|
+
seen: set[tuple[str, int, str]] = set()
|
|
42
|
+
for index, item in enumerate(raw_items):
|
|
43
|
+
if not isinstance(item, Mapping):
|
|
44
|
+
raise ValueError(f"corpus[{index}] must be a JSON object")
|
|
45
|
+
repo = str(item.get("repo") or "").strip()
|
|
46
|
+
if "/" not in repo:
|
|
47
|
+
raise ValueError(f"corpus[{index}].repo must be an owner/repo slug")
|
|
48
|
+
pr_number = parse_int(
|
|
49
|
+
item.get("pr_number", item.get("pr")),
|
|
50
|
+
field=f"corpus[{index}].pr_number",
|
|
51
|
+
)
|
|
52
|
+
head_sha = str(item.get("head_sha") or item.get("head") or "").strip()
|
|
53
|
+
key = (repo, pr_number, head_sha)
|
|
54
|
+
if key in seen:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"duplicate corpus PR entry: {repo}#{pr_number} "
|
|
57
|
+
f"{head_sha or '(head unspecified)'}"
|
|
58
|
+
)
|
|
59
|
+
seen.add(key)
|
|
60
|
+
source = str(item.get("source") or "known-pr")
|
|
61
|
+
expected_findings = list(item.get("expected_findings", []))
|
|
62
|
+
truth = normalize_truth(item, source=source)
|
|
63
|
+
expected_findings = list(truth.get("expected_findings") or expected_findings)
|
|
64
|
+
items.append(
|
|
65
|
+
{
|
|
66
|
+
"repo": repo,
|
|
67
|
+
"pr_number": pr_number,
|
|
68
|
+
"head_sha": head_sha,
|
|
69
|
+
"base_ref": str(item.get("base_ref") or ""),
|
|
70
|
+
"difficulty": str(item.get("difficulty") or "unknown"),
|
|
71
|
+
"review_class": str(item.get("review_class") or "general"),
|
|
72
|
+
"source": source,
|
|
73
|
+
"truth": truth,
|
|
74
|
+
"known_clean": truth["known_clean"],
|
|
75
|
+
"known_blocked": truth["known_blocked"],
|
|
76
|
+
"expected_findings": expected_findings,
|
|
77
|
+
"context_packs": list(item.get("context_packs", [])),
|
|
78
|
+
"reviewer_evidence": list(item.get("reviewer_evidence", [])),
|
|
79
|
+
"reviewer_runs": list(item.get("reviewer_runs", [])),
|
|
80
|
+
"reviewer_run_dispositions": list(
|
|
81
|
+
item.get("reviewer_run_dispositions", [])
|
|
82
|
+
),
|
|
83
|
+
"notes": str(item.get("notes") or ""),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return {
|
|
87
|
+
"version": 1,
|
|
88
|
+
"name": str(payload.get("name") or path.stem),
|
|
89
|
+
"description": str(payload.get("description") or ""),
|
|
90
|
+
"corpus": items,
|
|
91
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Calibration evidence classification constants."""
|
|
2
|
+
|
|
3
|
+
KNOWN_EVIDENCE_DISPOSITIONS = {
|
|
4
|
+
"true_positive",
|
|
5
|
+
"useful",
|
|
6
|
+
"false_positive",
|
|
7
|
+
"noise",
|
|
8
|
+
"unknown",
|
|
9
|
+
}
|
|
10
|
+
USEFUL_EVIDENCE_DISPOSITIONS = {"true_positive", "useful"}
|
|
11
|
+
NON_BLOCKING_CODERABBIT_SEVERITIES = {
|
|
12
|
+
"info",
|
|
13
|
+
"informational",
|
|
14
|
+
"low",
|
|
15
|
+
"minor",
|
|
16
|
+
"nit",
|
|
17
|
+
"notice",
|
|
18
|
+
"style",
|
|
19
|
+
"suggestion",
|
|
20
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Reviewer evidence report building for calibration corpora."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from .evidence import USEFUL_EVIDENCE_DISPOSITIONS
|
|
8
|
+
from .run_results import normalize_disposition
|
|
9
|
+
from .run_status import (
|
|
10
|
+
RUN_STATUS_AUDIT_INPUT_INSUFFICIENT,
|
|
11
|
+
RUN_STATUS_BLOCKED,
|
|
12
|
+
RUN_STATUS_INFRA_ERROR,
|
|
13
|
+
RUN_STATUS_PASS,
|
|
14
|
+
normalize_run_status_category,
|
|
15
|
+
)
|
|
16
|
+
from .truth import truth_for_item
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _reviewer_id(record: Mapping[str, Any]) -> str:
|
|
20
|
+
reviewer = str(
|
|
21
|
+
record.get("reviewer")
|
|
22
|
+
or record.get("profile_id")
|
|
23
|
+
or record.get("lane")
|
|
24
|
+
or "unknown-reviewer"
|
|
25
|
+
).strip()
|
|
26
|
+
return reviewer or "unknown-reviewer"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_reviewer_evidence_report(corpus: Mapping[str, Any]) -> dict[str, Any]:
|
|
30
|
+
"""Summarize adjudicated reviewer evidence embedded in a calibration corpus."""
|
|
31
|
+
|
|
32
|
+
profile_counts: dict[str, dict[str, int]] = {}
|
|
33
|
+
profile_runs: dict[str, set[tuple[Any, ...]]] = {}
|
|
34
|
+
profile_durations: dict[str, float] = {}
|
|
35
|
+
profile_clean_run_keys: dict[str, set[tuple[Any, ...]]] = {}
|
|
36
|
+
profile_blocking_false_positive_run_keys: dict[str, set[tuple[Any, ...]]] = {}
|
|
37
|
+
profile_known_blocked_run_keys: dict[str, set[tuple[Any, ...]]] = {}
|
|
38
|
+
profile_known_blocked_caught_run_keys: dict[str, set[tuple[Any, ...]]] = {}
|
|
39
|
+
profile_known_blocked_missed_run_keys: dict[str, set[tuple[Any, ...]]] = {}
|
|
40
|
+
profile_audit_input_insufficient_run_keys: dict[str, set[tuple[Any, ...]]] = {}
|
|
41
|
+
profile_infra_error_run_keys: dict[str, set[tuple[Any, ...]]] = {}
|
|
42
|
+
profile_run_statuses: dict[str, dict[str, int]] = {}
|
|
43
|
+
profile_review_classes: dict[str, set[str]] = {}
|
|
44
|
+
profile_context_packs: dict[str, set[str]] = {}
|
|
45
|
+
profile_useful_review_classes: dict[str, set[str]] = {}
|
|
46
|
+
profile_useful_context_packs: dict[str, set[str]] = {}
|
|
47
|
+
models: dict[str, str] = {}
|
|
48
|
+
findings: list[dict[str, Any]] = []
|
|
49
|
+
run_dispositions: list[dict[str, Any]] = []
|
|
50
|
+
run_records: list[dict[str, Any]] = []
|
|
51
|
+
|
|
52
|
+
for item in corpus.get("corpus", []) or []:
|
|
53
|
+
if not isinstance(item, Mapping):
|
|
54
|
+
continue
|
|
55
|
+
repo = str(item.get("repo") or "")
|
|
56
|
+
pr_number = int(item.get("pr_number") or 0)
|
|
57
|
+
head_sha = str(item.get("head_sha") or "")
|
|
58
|
+
review_class = str(item.get("review_class") or "general")
|
|
59
|
+
context_packs = [
|
|
60
|
+
str(pack)
|
|
61
|
+
for pack in item.get("context_packs", []) or []
|
|
62
|
+
if str(pack).strip()
|
|
63
|
+
]
|
|
64
|
+
truth = truth_for_item(item)
|
|
65
|
+
run_key = (repo, pr_number, head_sha)
|
|
66
|
+
|
|
67
|
+
for index, evidence in enumerate(item.get("reviewer_evidence", []) or []):
|
|
68
|
+
if not isinstance(evidence, Mapping):
|
|
69
|
+
continue
|
|
70
|
+
reviewer = _reviewer_id(evidence)
|
|
71
|
+
disposition = normalize_disposition(evidence.get("disposition"))
|
|
72
|
+
profile_review_classes.setdefault(reviewer, set()).add(review_class)
|
|
73
|
+
profile_context_packs.setdefault(reviewer, set()).update(context_packs)
|
|
74
|
+
if disposition in USEFUL_EVIDENCE_DISPOSITIONS:
|
|
75
|
+
profile_useful_review_classes.setdefault(reviewer, set()).add(review_class)
|
|
76
|
+
profile_useful_context_packs.setdefault(reviewer, set()).update(context_packs)
|
|
77
|
+
profile_counts.setdefault(reviewer, {})
|
|
78
|
+
profile_counts[reviewer][disposition] = (
|
|
79
|
+
profile_counts[reviewer].get(disposition, 0) + 1
|
|
80
|
+
)
|
|
81
|
+
profile_runs.setdefault(reviewer, set()).add(run_key)
|
|
82
|
+
if evidence.get("duration_seconds") is not None:
|
|
83
|
+
try:
|
|
84
|
+
profile_durations[reviewer] = profile_durations.get(
|
|
85
|
+
reviewer, 0.0
|
|
86
|
+
) + float(evidence.get("duration_seconds"))
|
|
87
|
+
except (TypeError, ValueError):
|
|
88
|
+
pass
|
|
89
|
+
if evidence.get("model"):
|
|
90
|
+
models[reviewer] = str(evidence.get("model"))
|
|
91
|
+
findings.append(
|
|
92
|
+
{
|
|
93
|
+
"profile_id": reviewer,
|
|
94
|
+
"repo": repo,
|
|
95
|
+
"pr_number": pr_number,
|
|
96
|
+
"head_sha": head_sha,
|
|
97
|
+
"difficulty": str(item.get("difficulty") or "unknown"),
|
|
98
|
+
"review_class": review_class,
|
|
99
|
+
"context_packs": context_packs,
|
|
100
|
+
"source": str(item.get("source") or ""),
|
|
101
|
+
"evidence_index": index,
|
|
102
|
+
"disposition": disposition,
|
|
103
|
+
"path": str(evidence.get("path") or ""),
|
|
104
|
+
"severity": str(evidence.get("severity") or ""),
|
|
105
|
+
"text": str(evidence.get("summary") or evidence.get("text") or ""),
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
for run_index, run in enumerate(item.get("reviewer_runs", []) or []):
|
|
110
|
+
if not isinstance(run, Mapping):
|
|
111
|
+
continue
|
|
112
|
+
reviewer = _reviewer_id(run)
|
|
113
|
+
profile_review_classes.setdefault(reviewer, set()).add(review_class)
|
|
114
|
+
profile_context_packs.setdefault(reviewer, set()).update(context_packs)
|
|
115
|
+
run_identity_parts = [
|
|
116
|
+
str(run.get("calibration_manifest_id") or "").strip(),
|
|
117
|
+
str(
|
|
118
|
+
run.get("calibration_run_id")
|
|
119
|
+
or run.get("run_id")
|
|
120
|
+
or run.get("replicate")
|
|
121
|
+
or ""
|
|
122
|
+
).strip(),
|
|
123
|
+
]
|
|
124
|
+
run_identity = "::".join(part for part in run_identity_parts if part)
|
|
125
|
+
reviewer_run_key: tuple[Any, ...] = (
|
|
126
|
+
(*run_key, run_identity) if run_identity else run_key
|
|
127
|
+
)
|
|
128
|
+
profile_runs.setdefault(reviewer, set()).add(reviewer_run_key)
|
|
129
|
+
if run.get("duration_seconds") is not None:
|
|
130
|
+
try:
|
|
131
|
+
profile_durations[reviewer] = profile_durations.get(
|
|
132
|
+
reviewer, 0.0
|
|
133
|
+
) + float(run.get("duration_seconds"))
|
|
134
|
+
except (TypeError, ValueError):
|
|
135
|
+
pass
|
|
136
|
+
if run.get("model"):
|
|
137
|
+
models[reviewer] = str(run.get("model"))
|
|
138
|
+
run_disposition = normalize_disposition(
|
|
139
|
+
run.get("disposition")
|
|
140
|
+
or (
|
|
141
|
+
run.get("adjudication", {}).get("disposition")
|
|
142
|
+
if isinstance(run.get("adjudication"), Mapping)
|
|
143
|
+
else None
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
status = str(run.get("status") or run.get("verdict") or "unknown").strip().lower()
|
|
147
|
+
status_category = normalize_run_status_category(status)
|
|
148
|
+
profile_run_statuses.setdefault(reviewer, {})
|
|
149
|
+
profile_run_statuses[reviewer][status] = (
|
|
150
|
+
profile_run_statuses[reviewer].get(status, 0) + 1
|
|
151
|
+
)
|
|
152
|
+
known_clean = bool(run.get("known_clean") or truth.get("known_clean"))
|
|
153
|
+
known_blocked = bool(run.get("known_blocked") or truth.get("known_blocked"))
|
|
154
|
+
try:
|
|
155
|
+
finding_count = int(run.get("finding_count") or 0)
|
|
156
|
+
except (TypeError, ValueError):
|
|
157
|
+
finding_count = 0
|
|
158
|
+
try:
|
|
159
|
+
expected_finding_matches = int(run.get("expected_finding_matches") or 0)
|
|
160
|
+
except (TypeError, ValueError):
|
|
161
|
+
expected_finding_matches = 0
|
|
162
|
+
if (
|
|
163
|
+
known_clean
|
|
164
|
+
and finding_count == 0
|
|
165
|
+
and status_category == RUN_STATUS_PASS
|
|
166
|
+
):
|
|
167
|
+
profile_clean_run_keys.setdefault(reviewer, set()).add(reviewer_run_key)
|
|
168
|
+
if known_clean and status_category == RUN_STATUS_BLOCKED:
|
|
169
|
+
profile_blocking_false_positive_run_keys.setdefault(
|
|
170
|
+
reviewer, set()
|
|
171
|
+
).add(reviewer_run_key)
|
|
172
|
+
expected_blocker_caught = bool(
|
|
173
|
+
run.get("expected_blocker_caught")
|
|
174
|
+
or run.get("caught_expected_blocker")
|
|
175
|
+
or (
|
|
176
|
+
run_disposition in USEFUL_EVIDENCE_DISPOSITIONS
|
|
177
|
+
and status_category == RUN_STATUS_BLOCKED
|
|
178
|
+
)
|
|
179
|
+
or expected_finding_matches > 0
|
|
180
|
+
)
|
|
181
|
+
evidence_disposition = run_disposition
|
|
182
|
+
evidence_notes = str(
|
|
183
|
+
run.get("disposition_notes")
|
|
184
|
+
or (
|
|
185
|
+
run.get("adjudication", {}).get("notes")
|
|
186
|
+
if isinstance(run.get("adjudication"), Mapping)
|
|
187
|
+
else ""
|
|
188
|
+
)
|
|
189
|
+
or ""
|
|
190
|
+
)
|
|
191
|
+
if evidence_disposition == "unknown" and expected_finding_matches > 0:
|
|
192
|
+
evidence_disposition = "true_positive"
|
|
193
|
+
evidence_notes = evidence_notes or "Matched an expected calibration finding."
|
|
194
|
+
if (
|
|
195
|
+
evidence_disposition == "unknown"
|
|
196
|
+
and known_clean
|
|
197
|
+
and status_category == RUN_STATUS_BLOCKED
|
|
198
|
+
):
|
|
199
|
+
evidence_disposition = "false_positive"
|
|
200
|
+
evidence_notes = evidence_notes or "Blocked a known-clean calibration control."
|
|
201
|
+
if evidence_disposition != "unknown":
|
|
202
|
+
profile_counts.setdefault(reviewer, {})
|
|
203
|
+
profile_counts[reviewer][evidence_disposition] = (
|
|
204
|
+
profile_counts[reviewer].get(evidence_disposition, 0) + 1
|
|
205
|
+
)
|
|
206
|
+
if evidence_disposition in USEFUL_EVIDENCE_DISPOSITIONS:
|
|
207
|
+
profile_useful_review_classes.setdefault(reviewer, set()).add(
|
|
208
|
+
review_class
|
|
209
|
+
)
|
|
210
|
+
profile_useful_context_packs.setdefault(reviewer, set()).update(
|
|
211
|
+
context_packs
|
|
212
|
+
)
|
|
213
|
+
run_dispositions.append(
|
|
214
|
+
{
|
|
215
|
+
"profile_id": reviewer,
|
|
216
|
+
"repo": repo,
|
|
217
|
+
"pr_number": pr_number,
|
|
218
|
+
"head_sha": head_sha,
|
|
219
|
+
"difficulty": str(item.get("difficulty") or "unknown"),
|
|
220
|
+
"review_class": review_class,
|
|
221
|
+
"context_packs": context_packs,
|
|
222
|
+
"source": str(item.get("source") or ""),
|
|
223
|
+
"run_index": run_index,
|
|
224
|
+
"disposition": evidence_disposition,
|
|
225
|
+
"inferred": run_disposition == "unknown",
|
|
226
|
+
"notes": evidence_notes,
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
if known_blocked:
|
|
230
|
+
profile_known_blocked_run_keys.setdefault(reviewer, set()).add(
|
|
231
|
+
reviewer_run_key
|
|
232
|
+
)
|
|
233
|
+
if expected_blocker_caught:
|
|
234
|
+
profile_known_blocked_caught_run_keys.setdefault(
|
|
235
|
+
reviewer, set()
|
|
236
|
+
).add(reviewer_run_key)
|
|
237
|
+
elif status_category in {RUN_STATUS_PASS, RUN_STATUS_BLOCKED}:
|
|
238
|
+
profile_known_blocked_missed_run_keys.setdefault(
|
|
239
|
+
reviewer, set()
|
|
240
|
+
).add(reviewer_run_key)
|
|
241
|
+
if status_category == RUN_STATUS_INFRA_ERROR:
|
|
242
|
+
profile_infra_error_run_keys.setdefault(reviewer, set()).add(
|
|
243
|
+
reviewer_run_key
|
|
244
|
+
)
|
|
245
|
+
if status_category == RUN_STATUS_AUDIT_INPUT_INSUFFICIENT:
|
|
246
|
+
profile_audit_input_insufficient_run_keys.setdefault(
|
|
247
|
+
reviewer, set()
|
|
248
|
+
).add(reviewer_run_key)
|
|
249
|
+
run_records.append(
|
|
250
|
+
{
|
|
251
|
+
"profile_id": reviewer,
|
|
252
|
+
"repo": repo,
|
|
253
|
+
"pr_number": pr_number,
|
|
254
|
+
"head_sha": head_sha,
|
|
255
|
+
"difficulty": str(item.get("difficulty") or "unknown"),
|
|
256
|
+
"review_class": review_class,
|
|
257
|
+
"context_packs": context_packs,
|
|
258
|
+
"source": str(item.get("source") or ""),
|
|
259
|
+
"run_index": run_index,
|
|
260
|
+
"status": status,
|
|
261
|
+
"status_category": status_category,
|
|
262
|
+
"known_clean": known_clean,
|
|
263
|
+
"known_blocked": known_blocked,
|
|
264
|
+
"finding_count": finding_count,
|
|
265
|
+
"expected_finding_matches": expected_finding_matches,
|
|
266
|
+
"expected_blocker_caught": expected_blocker_caught,
|
|
267
|
+
"disposition": evidence_disposition,
|
|
268
|
+
"duration_seconds": run.get("duration_seconds"),
|
|
269
|
+
"parse_status": str(run.get("parse_status") or ""),
|
|
270
|
+
"result_category": str(run.get("result_category") or ""),
|
|
271
|
+
"audit_input_insufficient_count": int(
|
|
272
|
+
run.get("audit_input_insufficient_count") or 0
|
|
273
|
+
),
|
|
274
|
+
"artifact": str(run.get("artifact") or ""),
|
|
275
|
+
"calibration_run_id": run_identity,
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
profiles = {
|
|
280
|
+
reviewer: {
|
|
281
|
+
"model": models.get(reviewer, ""),
|
|
282
|
+
"runs": len(profile_runs.get(reviewer, set())),
|
|
283
|
+
"duration_seconds_total": round(profile_durations.get(reviewer, 0.0), 3),
|
|
284
|
+
"dispositions": dict(sorted(counts.items())),
|
|
285
|
+
"finding_count": sum(counts.values()),
|
|
286
|
+
"known_clean_pass_runs": len(profile_clean_run_keys.get(reviewer, set())),
|
|
287
|
+
"blocking_false_positive_runs": len(
|
|
288
|
+
profile_blocking_false_positive_run_keys.get(reviewer, set())
|
|
289
|
+
),
|
|
290
|
+
"known_blocked_runs": len(profile_known_blocked_run_keys.get(reviewer, set())),
|
|
291
|
+
"known_blocked_caught_runs": len(
|
|
292
|
+
profile_known_blocked_caught_run_keys.get(reviewer, set())
|
|
293
|
+
),
|
|
294
|
+
"known_blocked_missed_runs": len(
|
|
295
|
+
profile_known_blocked_missed_run_keys.get(reviewer, set())
|
|
296
|
+
),
|
|
297
|
+
"infra_error_runs": len(profile_infra_error_run_keys.get(reviewer, set())),
|
|
298
|
+
"audit_input_insufficient_runs": len(
|
|
299
|
+
profile_audit_input_insufficient_run_keys.get(reviewer, set())
|
|
300
|
+
),
|
|
301
|
+
"run_statuses": dict(sorted(profile_run_statuses.get(reviewer, {}).items())),
|
|
302
|
+
"review_classes": sorted(profile_review_classes.get(reviewer, set())),
|
|
303
|
+
"context_packs": sorted(profile_context_packs.get(reviewer, set())),
|
|
304
|
+
"useful_review_classes": sorted(
|
|
305
|
+
profile_useful_review_classes.get(reviewer, set())
|
|
306
|
+
),
|
|
307
|
+
"useful_context_packs": sorted(
|
|
308
|
+
profile_useful_context_packs.get(reviewer, set())
|
|
309
|
+
),
|
|
310
|
+
}
|
|
311
|
+
for reviewer in sorted(set(profile_counts) | set(profile_runs))
|
|
312
|
+
for counts in [profile_counts.get(reviewer, {})]
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
"mode": "reviewer-evidence-calibration",
|
|
316
|
+
"corpus_name": corpus.get("name", ""),
|
|
317
|
+
"description": corpus.get("description", ""),
|
|
318
|
+
"source_item_count": len(corpus.get("corpus", []) or []),
|
|
319
|
+
"evidence_count": len(findings) + len(run_dispositions),
|
|
320
|
+
"finding_evidence_count": len(findings),
|
|
321
|
+
"run_disposition_count": len(run_dispositions),
|
|
322
|
+
"sources": [str(corpus.get("name", "calibration-corpus"))],
|
|
323
|
+
"profiles": profiles,
|
|
324
|
+
"findings": findings,
|
|
325
|
+
"run_dispositions": run_dispositions,
|
|
326
|
+
"reviewer_runs": run_records,
|
|
327
|
+
"caveat": (
|
|
328
|
+
"This report summarizes historical adjudicated evidence embedded in "
|
|
329
|
+
"the corpus. Use it to bootstrap calibration, then confirm with fresh "
|
|
330
|
+
"blind runs before promoting lanes."
|
|
331
|
+
),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def render_evidence_text(report: Mapping[str, Any]) -> str:
|
|
336
|
+
lines = [
|
|
337
|
+
"Code Mower reviewer evidence",
|
|
338
|
+
f"Corpus: {report.get('corpus_name', '')}",
|
|
339
|
+
f"Adjudicated evidence: {report.get('evidence_count', 0)}",
|
|
340
|
+
f"Finding evidence: {report.get('finding_evidence_count', 0)}",
|
|
341
|
+
f"Run dispositions: {report.get('run_disposition_count', 0)}",
|
|
342
|
+
"",
|
|
343
|
+
"Profiles:",
|
|
344
|
+
]
|
|
345
|
+
profiles = report.get("profiles", {})
|
|
346
|
+
if isinstance(profiles, Mapping) and profiles:
|
|
347
|
+
for profile_id, stats in profiles.items():
|
|
348
|
+
if not isinstance(stats, Mapping):
|
|
349
|
+
continue
|
|
350
|
+
dispositions = stats.get("dispositions", {})
|
|
351
|
+
lines.append(
|
|
352
|
+
f"- {profile_id}: runs={stats.get('runs', 0)} "
|
|
353
|
+
f"findings={stats.get('finding_count', 0)} "
|
|
354
|
+
f"clean_passes={stats.get('known_clean_pass_runs', 0)} "
|
|
355
|
+
f"dispositions={dispositions}"
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
lines.append("- none")
|
|
359
|
+
lines.extend(["", f"Caveat: {report.get('caveat', '')}"])
|
|
360
|
+
return "\n".join(lines) + "\n"
|