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