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
code_mower/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""SaaS reviewer adapter package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from ._base import SaaSReviewerAdapter
|
|
9
|
+
except ImportError: # pragma: no cover - direct `python tools/foo.py` execution
|
|
10
|
+
try:
|
|
11
|
+
from tools.adapters._base import SaaSReviewerAdapter
|
|
12
|
+
except ImportError:
|
|
13
|
+
from adapters._base import SaaSReviewerAdapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_adapter(name: str) -> SaaSReviewerAdapter:
|
|
17
|
+
normalized = name.replace("-", "_")
|
|
18
|
+
if normalized not in {"cursor_bugbot", "greptile", "gitar", "qodo"}:
|
|
19
|
+
raise ValueError(f"unknown SaaS reviewer adapter: {name}")
|
|
20
|
+
module_roots = []
|
|
21
|
+
if __package__:
|
|
22
|
+
module_roots.append(__package__)
|
|
23
|
+
module_roots.extend(["tools.adapters", "adapters"])
|
|
24
|
+
seen_roots: set[str] = set()
|
|
25
|
+
last_missing: ModuleNotFoundError | None = None
|
|
26
|
+
for root in module_roots:
|
|
27
|
+
if root in seen_roots:
|
|
28
|
+
continue
|
|
29
|
+
seen_roots.add(root)
|
|
30
|
+
module_name = f"{root}.{normalized}"
|
|
31
|
+
try:
|
|
32
|
+
module = import_module(module_name)
|
|
33
|
+
return module.ADAPTER
|
|
34
|
+
except ModuleNotFoundError as exc:
|
|
35
|
+
if exc.name == module_name or exc.name == root.split(".", 1)[0]:
|
|
36
|
+
last_missing = exc
|
|
37
|
+
continue
|
|
38
|
+
raise
|
|
39
|
+
raise ValueError(f"unknown SaaS reviewer adapter: {name}") from last_missing
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Base contract for SaaS reviewer labeler adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SaaSReviewerAdapter(ABC):
|
|
11
|
+
"""Interface for advisory SaaS code-review labelers."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
"""Short CLI adapter name, e.g. "greptile"."""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def label_prefix(self) -> str:
|
|
21
|
+
"""Audit label prefix, e.g. "greptile-audit"."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def bot_usernames(self) -> frozenset[str]:
|
|
26
|
+
"""Default trusted bot author logins."""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def bot_authors_env_var(self) -> str:
|
|
31
|
+
"""Optional comma-separated override env var for bot authors."""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def event_type(self) -> str:
|
|
36
|
+
"""GitHub event type this adapter handles."""
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def supported_event_types(self) -> frozenset[str]:
|
|
40
|
+
event_types = {self.event_type}
|
|
41
|
+
if self.check_run_app_slugs or self.check_run_names:
|
|
42
|
+
event_types.add("check_run")
|
|
43
|
+
return frozenset(event_types)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def needs_label(self) -> str:
|
|
47
|
+
return f"needs-{self.label_prefix}"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def done_label(self) -> str:
|
|
51
|
+
return f"{self.label_prefix}-done"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def blocked_label(self) -> str:
|
|
55
|
+
return f"{self.label_prefix}-blocked"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def opt_in_required(self) -> bool:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def opt_in_label_names(self) -> frozenset[str]:
|
|
63
|
+
return frozenset()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def opt_in_label_prefixes(self) -> frozenset[str]:
|
|
67
|
+
return frozenset()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def informational_only(self) -> bool:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def requires_review_comments(self) -> bool:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def review_comments_page_cap(self) -> int:
|
|
79
|
+
return 50
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def token_env_vars(self) -> tuple[str, ...]:
|
|
83
|
+
return ("GITHUB_TOKEN",)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def check_run_app_slugs(self) -> frozenset[str]:
|
|
87
|
+
return frozenset()
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def check_run_names(self) -> frozenset[str]:
|
|
91
|
+
return frozenset()
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def check_run_done_requires_absent_same_head_review(self) -> bool:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def same_head_review_exists_reason(self) -> str:
|
|
98
|
+
return (
|
|
99
|
+
f"check_run success ignored: {self.name} PR review already exists "
|
|
100
|
+
"for this head; review event owns the verdict"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def review_lookup_failed_reason(self, exc: Exception) -> str:
|
|
104
|
+
return f"could not verify absence of same-head {self.name} PR review: {exc}"
|
|
105
|
+
|
|
106
|
+
def review_authors(self) -> frozenset[str]:
|
|
107
|
+
raw_authors = os.environ.get(self.bot_authors_env_var) or ",".join(
|
|
108
|
+
sorted(self.bot_usernames)
|
|
109
|
+
)
|
|
110
|
+
return frozenset(
|
|
111
|
+
author.strip().lower() for author in raw_authors.split(",") if author.strip()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def is_review_author(self, login: str) -> bool:
|
|
115
|
+
return login.lower() in self.review_authors()
|
|
116
|
+
|
|
117
|
+
def is_check_run_author(self, check_run: dict[str, Any]) -> bool:
|
|
118
|
+
app = check_run.get("app") or {}
|
|
119
|
+
slug = str(app.get("slug") or "").lower()
|
|
120
|
+
return bool(slug and slug in self.check_run_app_slugs)
|
|
121
|
+
|
|
122
|
+
def is_check_run_name(self, check_run: dict[str, Any]) -> bool:
|
|
123
|
+
allowed = self.check_run_names
|
|
124
|
+
if not allowed:
|
|
125
|
+
return False
|
|
126
|
+
return str(check_run.get("name") or "").strip().lower() in allowed
|
|
127
|
+
|
|
128
|
+
def is_opted_in(self, labels: list[str]) -> bool:
|
|
129
|
+
if not self.opt_in_required:
|
|
130
|
+
return True
|
|
131
|
+
label_set = set(labels)
|
|
132
|
+
if label_set & self.opt_in_label_names:
|
|
133
|
+
return True
|
|
134
|
+
return any(
|
|
135
|
+
label.startswith(prefix)
|
|
136
|
+
for label in label_set
|
|
137
|
+
for prefix in self.opt_in_label_prefixes
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def classify_verdict(self, event: dict[str, Any], **kwargs: Any) -> Optional[str]:
|
|
142
|
+
"""Return "done", "blocked", "needs", or None for no adapter verdict."""
|
|
143
|
+
|
|
144
|
+
def extract_stale_head_info(self, event: dict[str, Any]) -> tuple[Optional[str], bool]:
|
|
145
|
+
return None, False
|
|
146
|
+
|
|
147
|
+
def label_reason(self, status: str, event: dict[str, Any], **kwargs: Any) -> str:
|
|
148
|
+
return f"{self.name} verdict: {status}"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Cursor BugBot adapter for the generic SaaS reviewer labeler.
|
|
2
|
+
|
|
3
|
+
BugBot is currently treated as an informational, manually triggered lane. This
|
|
4
|
+
adapter is intentionally conservative: the disabled-repository response is
|
|
5
|
+
classified as needing setup, an explicit no-bugs response can mark the lane
|
|
6
|
+
done, and all other trusted BugBot comments fail closed to ``needs`` until
|
|
7
|
+
real enabled-output examples are captured and calibrated.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from ._base import SaaSReviewerAdapter
|
|
17
|
+
except ImportError: # pragma: no cover - direct `python tools/foo.py` execution
|
|
18
|
+
try:
|
|
19
|
+
from tools.adapters._base import SaaSReviewerAdapter
|
|
20
|
+
except ImportError:
|
|
21
|
+
from adapters._base import SaaSReviewerAdapter
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_DISABLED_RE = re.compile(r"\bBugbot is disabled for this repository\b", re.IGNORECASE)
|
|
25
|
+
_NO_BUGS_RE = re.compile(
|
|
26
|
+
r"\A\s*(?:BugBot\s+)?(?:no\s+(?:bugs?|issues?)\s+(?:were\s+)?found|found\s+no\s+(?:bugs?|issues?))"
|
|
27
|
+
r"(?:\s+(?:in|for)\s+this\s+(?:pull\s+request|pr))?[.!]?\s*\Z",
|
|
28
|
+
re.IGNORECASE,
|
|
29
|
+
)
|
|
30
|
+
_FINDING_HINT_RE = re.compile(
|
|
31
|
+
r"\b(?:found\s+[1-9]\d*\s+(?:bugs?|issues?|findings?)|"
|
|
32
|
+
r"(?:bug|issue|finding|severity)\s*[:#]|changes\s+requested|p[0-3]\b)",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def classify_cursor_bugbot_comment(comment_body: str) -> tuple[Optional[str], str]:
|
|
38
|
+
if _DISABLED_RE.search(comment_body):
|
|
39
|
+
return "needs", "BugBot is disabled for this repository"
|
|
40
|
+
if _NO_BUGS_RE.search(comment_body) and not _FINDING_HINT_RE.search(comment_body):
|
|
41
|
+
return "done", "BugBot reported no bugs"
|
|
42
|
+
if "bugbot" not in comment_body.lower() and "cursor" not in comment_body.lower():
|
|
43
|
+
return None, "no Cursor BugBot marker"
|
|
44
|
+
return "needs", "Cursor BugBot output shape is not calibrated yet"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CursorBugBotAdapter(SaaSReviewerAdapter):
|
|
48
|
+
@property
|
|
49
|
+
def name(self) -> str:
|
|
50
|
+
return "cursor_bugbot"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def label_prefix(self) -> str:
|
|
54
|
+
return "cursor-bugbot-audit"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def bot_usernames(self) -> frozenset[str]:
|
|
58
|
+
return frozenset({"cursor[bot]", "cursor"})
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def bot_authors_env_var(self) -> str:
|
|
62
|
+
return "CURSOR_BUGBOT_BOT_AUTHORS"
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def event_type(self) -> str:
|
|
66
|
+
return "issue_comment"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def opt_in_required(self) -> bool:
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def opt_in_label_names(self) -> frozenset[str]:
|
|
74
|
+
return frozenset({self.needs_label, self.done_label, self.blocked_label})
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def opt_in_label_prefixes(self) -> frozenset[str]:
|
|
78
|
+
return frozenset({"cursor-bugbot-audit-"})
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def token_env_vars(self) -> tuple[str, ...]:
|
|
82
|
+
return ("CURSOR_BUGBOT_AUDIT_LABEL_TOKEN", "GITHUB_TOKEN")
|
|
83
|
+
|
|
84
|
+
def classify_verdict(self, event: dict[str, Any], **kwargs: Any) -> Optional[str]:
|
|
85
|
+
status, _ = classify_cursor_bugbot_comment(kwargs.get("comment_body") or "")
|
|
86
|
+
return status
|
|
87
|
+
|
|
88
|
+
def label_reason(self, status: str, event: dict[str, Any], **kwargs: Any) -> str:
|
|
89
|
+
_, detail = classify_cursor_bugbot_comment(kwargs.get("comment_body") or "")
|
|
90
|
+
if status == "needs":
|
|
91
|
+
return f"Cursor BugBot needs attention ({detail})"
|
|
92
|
+
if status == "done":
|
|
93
|
+
return f"Cursor BugBot clean ({detail})"
|
|
94
|
+
return f"Cursor BugBot verdict: {status} ({detail})"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
ADAPTER = CursorBugBotAdapter()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Gitar adapter for the generic SaaS reviewer labeler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from ._base import SaaSReviewerAdapter
|
|
10
|
+
except ImportError: # pragma: no cover - direct `python tools/foo.py` execution
|
|
11
|
+
try:
|
|
12
|
+
from tools.adapters._base import SaaSReviewerAdapter
|
|
13
|
+
except ImportError:
|
|
14
|
+
from adapters._base import SaaSReviewerAdapter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_CODE_REVIEW_LABEL_RE = re.compile(r"Code Review\s*</b>", re.IGNORECASE)
|
|
18
|
+
_CODE_REVIEW_BADGE_RE = re.compile(
|
|
19
|
+
r"Code Review\s*</b>\s*<kbd[^>]*>\s*(?P<badge>.*?)\s*</kbd>",
|
|
20
|
+
re.IGNORECASE | re.DOTALL,
|
|
21
|
+
)
|
|
22
|
+
_CODE_FENCE_RE = re.compile(r"(`+).*?\1|~~~.*?~~~", re.DOTALL)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _strip_code(text: str) -> str:
|
|
26
|
+
"""Blank code spans/fences so quoted badges cannot spoof verdicts."""
|
|
27
|
+
return _CODE_FENCE_RE.sub(lambda match: re.sub(r"[^\n]", " ", match.group(0)), text)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def has_code_review_block(comment_body: str) -> bool:
|
|
31
|
+
return bool(_CODE_REVIEW_LABEL_RE.search(comment_body))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_code_review_badge(comment_body: str) -> Optional[str]:
|
|
35
|
+
match = _CODE_REVIEW_BADGE_RE.search(comment_body)
|
|
36
|
+
if not match:
|
|
37
|
+
return None
|
|
38
|
+
badge = re.sub(r"<[^>]*>", "", match.group("badge"))
|
|
39
|
+
return badge.strip()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def classify_badge(badge: Optional[str]) -> Optional[str]:
|
|
43
|
+
if badge is None:
|
|
44
|
+
return None
|
|
45
|
+
if not badge:
|
|
46
|
+
return "needs"
|
|
47
|
+
normalized = re.sub(r"[^a-z]", "", badge.lower())
|
|
48
|
+
if normalized in {"approved", "approvedwithsuggestions"}:
|
|
49
|
+
return "done"
|
|
50
|
+
return "blocked"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def classify_gitar_comment(comment_body: str) -> tuple[Optional[str], str]:
|
|
54
|
+
body = _strip_code(comment_body)
|
|
55
|
+
if not has_code_review_block(body):
|
|
56
|
+
return None, "no Code Review block"
|
|
57
|
+
badge = parse_code_review_badge(body)
|
|
58
|
+
if not badge:
|
|
59
|
+
return "needs", "Code Review block present but badge unparseable"
|
|
60
|
+
return classify_badge(badge), f"Code Review badge {badge!r}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GitarAdapter(SaaSReviewerAdapter):
|
|
64
|
+
@property
|
|
65
|
+
def name(self) -> str:
|
|
66
|
+
return "gitar"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def label_prefix(self) -> str:
|
|
70
|
+
return "gitar-audit"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def bot_usernames(self) -> frozenset[str]:
|
|
74
|
+
return frozenset({"gitar-ai[bot]", "gitar-bot", "gitar-bot[bot]"})
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def bot_authors_env_var(self) -> str:
|
|
78
|
+
return "GITAR_BOT_AUTHORS"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def event_type(self) -> str:
|
|
82
|
+
return "issue_comment"
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def opt_in_required(self) -> bool:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def opt_in_label_names(self) -> frozenset[str]:
|
|
90
|
+
return frozenset({self.needs_label, self.done_label, self.blocked_label})
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def opt_in_label_prefixes(self) -> frozenset[str]:
|
|
94
|
+
return frozenset({"gitar-audit-"})
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def token_env_vars(self) -> tuple[str, ...]:
|
|
98
|
+
return ("GITAR_AUDIT_LABEL_TOKEN", "GITHUB_TOKEN")
|
|
99
|
+
|
|
100
|
+
def classify_verdict(self, event: dict[str, Any], **kwargs: Any) -> Optional[str]:
|
|
101
|
+
status, _ = classify_gitar_comment(kwargs.get("comment_body") or "")
|
|
102
|
+
return status
|
|
103
|
+
|
|
104
|
+
def label_reason(self, status: str, event: dict[str, Any], **kwargs: Any) -> str:
|
|
105
|
+
_, detail = classify_gitar_comment(kwargs.get("comment_body") or "")
|
|
106
|
+
if status == "needs":
|
|
107
|
+
return f"Gitar Code Review block present but verdict was empty/unparseable ({detail})"
|
|
108
|
+
return f"Gitar verdict: {status} ({detail})"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
ADAPTER = GitarAdapter()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Greptile adapter for the generic SaaS reviewer labeler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from ._base import SaaSReviewerAdapter
|
|
11
|
+
except ImportError: # pragma: no cover - direct `python tools/foo.py` execution
|
|
12
|
+
try:
|
|
13
|
+
from tools.adapters._base import SaaSReviewerAdapter
|
|
14
|
+
except ImportError:
|
|
15
|
+
from adapters._base import SaaSReviewerAdapter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_ALT_P_TAG_RE = re.compile(r'alt="P([0-3])"', flags=re.IGNORECASE)
|
|
19
|
+
_BADGE_URL_RE = re.compile(
|
|
20
|
+
r"greptile-static-assets\.s3\.amazonaws\.com/badges/p([0-3])\.svg",
|
|
21
|
+
flags=re.IGNORECASE,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def severity_of(comment_body: str) -> Optional[int]:
|
|
26
|
+
"""Return 0/1/2/3 if a Greptile inline comment has a P-badge."""
|
|
27
|
+
match = _ALT_P_TAG_RE.search(comment_body)
|
|
28
|
+
if match:
|
|
29
|
+
return int(match.group(1))
|
|
30
|
+
match = _BADGE_URL_RE.search(comment_body)
|
|
31
|
+
if match:
|
|
32
|
+
return int(match.group(1))
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class GreptileVerdict:
|
|
38
|
+
p0_count: int = 0
|
|
39
|
+
p1_count: int = 0
|
|
40
|
+
p2_count: int = 0
|
|
41
|
+
p3_count: int = 0
|
|
42
|
+
unparsed_count: int = 0
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def total_comments(self) -> int:
|
|
46
|
+
return (
|
|
47
|
+
self.p0_count
|
|
48
|
+
+ self.p1_count
|
|
49
|
+
+ self.p2_count
|
|
50
|
+
+ self.p3_count
|
|
51
|
+
+ self.unparsed_count
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def blocker_count(self) -> int:
|
|
56
|
+
return self.p0_count + self.p1_count
|
|
57
|
+
|
|
58
|
+
def classify(self) -> str:
|
|
59
|
+
if self.unparsed_count > 0:
|
|
60
|
+
return "needs"
|
|
61
|
+
if self.blocker_count > 0:
|
|
62
|
+
return "blocked"
|
|
63
|
+
return "done"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_review_comments(comments: list[dict[str, Any]]) -> GreptileVerdict:
|
|
67
|
+
p0_count = p1_count = p2_count = p3_count = unparsed_count = 0
|
|
68
|
+
for comment in comments:
|
|
69
|
+
severity = severity_of(comment.get("body") or "")
|
|
70
|
+
if severity is None:
|
|
71
|
+
unparsed_count += 1
|
|
72
|
+
elif severity == 0:
|
|
73
|
+
p0_count += 1
|
|
74
|
+
elif severity == 1:
|
|
75
|
+
p1_count += 1
|
|
76
|
+
elif severity == 2:
|
|
77
|
+
p2_count += 1
|
|
78
|
+
elif severity == 3:
|
|
79
|
+
p3_count += 1
|
|
80
|
+
return GreptileVerdict(
|
|
81
|
+
p0_count=p0_count,
|
|
82
|
+
p1_count=p1_count,
|
|
83
|
+
p2_count=p2_count,
|
|
84
|
+
p3_count=p3_count,
|
|
85
|
+
unparsed_count=unparsed_count,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class GreptileAdapter(SaaSReviewerAdapter):
|
|
90
|
+
@property
|
|
91
|
+
def name(self) -> str:
|
|
92
|
+
return "greptile"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def label_prefix(self) -> str:
|
|
96
|
+
return "greptile-audit"
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def bot_usernames(self) -> frozenset[str]:
|
|
100
|
+
return frozenset({"greptile-apps", "greptile-apps[bot]"})
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def bot_authors_env_var(self) -> str:
|
|
104
|
+
return "GREPTILE_BOT_AUTHORS"
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def event_type(self) -> str:
|
|
108
|
+
return "pull_request_review"
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def opt_in_required(self) -> bool:
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def opt_in_label_names(self) -> frozenset[str]:
|
|
116
|
+
return frozenset({self.needs_label, self.done_label, self.blocked_label})
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def opt_in_label_prefixes(self) -> frozenset[str]:
|
|
120
|
+
return frozenset({"greptile-audit-"})
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def requires_review_comments(self) -> bool:
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def token_env_vars(self) -> tuple[str, ...]:
|
|
128
|
+
return ("GREPTILE_AUDIT_LABEL_TOKEN", "GITHUB_TOKEN")
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def check_run_app_slugs(self) -> frozenset[str]:
|
|
132
|
+
return frozenset({"greptile-apps"})
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def check_run_names(self) -> frozenset[str]:
|
|
136
|
+
return frozenset({"greptile review"})
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def check_run_done_requires_absent_same_head_review(self) -> bool:
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
def classify_verdict(self, event: dict[str, Any], **kwargs: Any) -> Optional[str]:
|
|
143
|
+
check_run = kwargs.get("check_run")
|
|
144
|
+
if isinstance(check_run, dict):
|
|
145
|
+
conclusion = str(check_run.get("conclusion") or "").lower()
|
|
146
|
+
if conclusion == "success":
|
|
147
|
+
return "done"
|
|
148
|
+
if conclusion in {"failure", "timed_out", "action_required", "startup_failure"}:
|
|
149
|
+
return "blocked"
|
|
150
|
+
# Any other or unknown conclusion fails closed to needs.
|
|
151
|
+
return "needs"
|
|
152
|
+
return parse_review_comments(kwargs.get("review_comments") or []).classify()
|
|
153
|
+
|
|
154
|
+
def extract_stale_head_info(self, event: dict[str, Any]) -> tuple[Optional[str], bool]:
|
|
155
|
+
review = event.get("review") or {}
|
|
156
|
+
return review.get("commit_id") or None, True
|
|
157
|
+
|
|
158
|
+
def label_reason(self, status: str, event: dict[str, Any], **kwargs: Any) -> str:
|
|
159
|
+
check_run = kwargs.get("check_run")
|
|
160
|
+
if isinstance(check_run, dict):
|
|
161
|
+
conclusion = str(check_run.get("conclusion") or "unknown")
|
|
162
|
+
head_sha = str(check_run.get("head_sha") or "")[:8]
|
|
163
|
+
suffix = f"; checked {head_sha}" if head_sha else ""
|
|
164
|
+
if status == "done":
|
|
165
|
+
return f"Greptile status check succeeded{suffix}"
|
|
166
|
+
if status == "blocked":
|
|
167
|
+
return f"Greptile status check concluded {conclusion}{suffix}"
|
|
168
|
+
return f"Greptile status check conclusion was not a final pass: {conclusion}{suffix}"
|
|
169
|
+
|
|
170
|
+
verdict = parse_review_comments(kwargs.get("review_comments") or [])
|
|
171
|
+
review_sha = ((event.get("review") or {}).get("commit_id") or "")[:8]
|
|
172
|
+
if status == "needs":
|
|
173
|
+
return (
|
|
174
|
+
f"{verdict.total_comments} inline comments present but at least one "
|
|
175
|
+
"has no recognizable P-badge (alt='P[N]' or badge URL); "
|
|
176
|
+
"Greptile output format may have drifted"
|
|
177
|
+
)
|
|
178
|
+
if status == "blocked":
|
|
179
|
+
return (
|
|
180
|
+
f"Greptile found {verdict.blocker_count} blocker(s) "
|
|
181
|
+
f"(P0={verdict.p0_count}, P1={verdict.p1_count}); "
|
|
182
|
+
f"P2={verdict.p2_count}, P3={verdict.p3_count} concerns"
|
|
183
|
+
)
|
|
184
|
+
suffix = f"; reviewed {review_sha}" if review_sha else ""
|
|
185
|
+
return (
|
|
186
|
+
f"Greptile audit clean (0 P0/P1 blockers; "
|
|
187
|
+
f"P2={verdict.p2_count}, P3={verdict.p3_count} concerns{suffix})"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
ADAPTER = GreptileAdapter()
|