feat(hermes): make the fix categories an operator setting
All checks were successful
Tests / Declarative: Post Actions passed: 1237
All checks were successful
Tests / Declarative: Post Actions passed: 1237
The three defect categories were implicit in code: an operator could neither see which were active nor switch one off. ARIADNE_HERMES_FIX_CATEGORIES now names them, an unrecognised entry is ignored rather than trusted, and an empty setting means all three so the default stays obvious. Kept separate from ARIADNE_HERMES_ALLOWED_ACTIONS on purpose. That allowlist gates what Ariadne executes on its own authority, with nothing between the decision and the change. Everything here becomes a pull request a person reads. Sharing one list would let a patch clear the same gate as an autonomous mutation, and that distinction is what makes the autonomous half defensible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c6aa448ee8
commit
54de192c8b
@ -241,20 +241,47 @@ def _is_writable(path: str, cfg: dict) -> bool:
|
||||
return bool(prefixes) and any(path.startswith(p) for p in prefixes)
|
||||
|
||||
|
||||
def extract_defects(bundle: dict, cfg: dict) -> list[dict[str, Any]]:
|
||||
"""Return every mechanically-fixable defect the evidence names.
|
||||
ALL_CATEGORIES: tuple[str, ...] = (LINT_VIOLATION, UNDEFINED_NAME, FAILING_ASSERTION)
|
||||
|
||||
Inputs: the evidence bundle and the per-job code cfg. Outputs: the lint,
|
||||
undefined-name and failing-assertion defects together, most precisely
|
||||
located first, so the instruction leads with the defect whose fix is least
|
||||
open to interpretation.
|
||||
|
||||
def enabled_categories(cfg: dict) -> tuple[str, ...]:
|
||||
"""Return the defect categories this deployment allows Hermes to be asked about.
|
||||
|
||||
Inputs: the per-job code cfg, whose `fix_categories` names the enabled
|
||||
ones. Outputs: the recognised subset, in declaration order.
|
||||
|
||||
This is the operator's control over what Hermes may be pointed at. It is
|
||||
deliberately separate from the action allowlist: that gates what Ariadne
|
||||
executes on its own, whereas everything here becomes a pull request a
|
||||
person reads. An unrecognised name is ignored rather than trusted, and an
|
||||
empty setting means every category, so the default stays obvious.
|
||||
"""
|
||||
|
||||
return (
|
||||
extract_lint_defects(bundle, cfg)
|
||||
+ extract_name_defects(bundle, cfg)
|
||||
+ extract_assertion_defects(bundle, cfg)
|
||||
)
|
||||
raw = cfg.get("fix_categories") if isinstance(cfg, dict) else None
|
||||
names = {str(item).strip() for item in (raw or []) if str(item).strip()}
|
||||
if not names:
|
||||
return ALL_CATEGORIES
|
||||
return tuple(category for category in ALL_CATEGORIES if category in names)
|
||||
|
||||
|
||||
def extract_defects(bundle: dict, cfg: dict) -> list[dict[str, Any]]:
|
||||
"""Return every enabled mechanically-fixable defect the evidence names.
|
||||
|
||||
Inputs: the evidence bundle and the per-job code cfg. Outputs: the enabled
|
||||
categories' defects together, most precisely located first, so the
|
||||
instruction leads with the defect whose fix is least open to
|
||||
interpretation.
|
||||
"""
|
||||
|
||||
enabled = enabled_categories(cfg)
|
||||
found: list[dict[str, Any]] = []
|
||||
if LINT_VIOLATION in enabled:
|
||||
found += extract_lint_defects(bundle, cfg)
|
||||
if UNDEFINED_NAME in enabled:
|
||||
found += extract_name_defects(bundle, cfg)
|
||||
if FAILING_ASSERTION in enabled:
|
||||
found += extract_assertion_defects(bundle, cfg)
|
||||
return found
|
||||
|
||||
|
||||
def defect_instruction(defects: list[dict[str, Any]]) -> str:
|
||||
|
||||
@ -48,6 +48,7 @@ def build_config(config: Any) -> dict[str, Any]:
|
||||
"owner": config.hermes_code_owner,
|
||||
"repo": config.hermes_code_repo,
|
||||
"max_open_proposals": getattr(config, "hermes_code_max_open_proposals", 0),
|
||||
"fix_categories": list(getattr(config, "hermes_fix_categories", None) or []),
|
||||
"base_branch": config.hermes_code_base_branch,
|
||||
"timeout_seconds": _GITEA_TIMEOUT_SECONDS,
|
||||
"legacy_job": str(getattr(config, "hermes_code_job", "") or ""),
|
||||
|
||||
@ -193,6 +193,7 @@ class Settings:
|
||||
hermes_max_branches: int
|
||||
hermes_job_namespaces: dict
|
||||
hermes_code_max_open_proposals: int
|
||||
hermes_fix_categories: list
|
||||
hermes_max_actions_per_incident: int
|
||||
hermes_api_url: str
|
||||
hermes_api_key: str
|
||||
|
||||
@ -41,6 +41,9 @@ def _hermes_autotriage_config() -> dict[str, Any]:
|
||||
"hermes_max_branches": _env_int("ARIADNE_HERMES_MAX_BRANCHES", 5),
|
||||
"hermes_job_namespaces": _pair_map(_env("ARIADNE_HERMES_JOB_NAMESPACES", "")),
|
||||
"hermes_code_max_open_proposals": _env_int("ARIADNE_HERMES_CODE_MAX_OPEN_PROPOSALS", 64),
|
||||
"hermes_fix_categories": [
|
||||
item.strip() for item in _env("ARIADNE_HERMES_FIX_CATEGORIES", "").split(",") if item.strip()
|
||||
],
|
||||
"hermes_max_actions_per_incident": _env_int("ARIADNE_HERMES_MAX_ACTIONS_PER_INCIDENT", 1),
|
||||
"hermes_api_url": _env(
|
||||
"ARIADNE_HERMES_API_URL",
|
||||
|
||||
@ -271,3 +271,39 @@ def test_a_lint_defect_with_a_disallowed_suffix_is_dropped() -> None:
|
||||
|
||||
cfg = {"allowed_path_prefixes": ["ariadne/"], "allowed_suffixes": [".go"]}
|
||||
assert module.extract_lint_defects(_bundle("ariadne/app.py:1:1: E501 Long"), cfg) == []
|
||||
|
||||
|
||||
# --- the operator's control over what Hermes may be asked to fix ---------------
|
||||
|
||||
|
||||
def test_no_setting_means_every_category() -> None:
|
||||
"""The default must be obvious rather than silently empty."""
|
||||
|
||||
assert module.enabled_categories({}) == module.ALL_CATEGORIES
|
||||
assert module.enabled_categories({"fix_categories": []}) == module.ALL_CATEGORIES
|
||||
assert module.enabled_categories(None) == module.ALL_CATEGORIES
|
||||
|
||||
|
||||
def test_a_category_can_be_switched_off() -> None:
|
||||
cfg = {**CFG, "fix_categories": [module.LINT_VIOLATION]}
|
||||
assert module.enabled_categories(cfg) == (module.LINT_VIOLATION,)
|
||||
|
||||
bundle = {
|
||||
"jenkins": {
|
||||
"console_failures": [{"text": "ariadne/app.py:12:5: E501 Line too long"}],
|
||||
"console_tail": "",
|
||||
"failed_tests": [{"name": "t", "className": "c", "errorDetails": "boom"}],
|
||||
}
|
||||
}
|
||||
cats = {d["category"] for d in module.extract_defects(bundle, cfg)}
|
||||
assert cats == {module.LINT_VIOLATION}
|
||||
|
||||
|
||||
def test_an_unrecognised_category_is_ignored_not_trusted() -> None:
|
||||
cfg = {"fix_categories": ["lint_violation", "rewrite_everything"]}
|
||||
assert module.enabled_categories(cfg) == (module.LINT_VIOLATION,)
|
||||
|
||||
|
||||
def test_categories_keep_their_declared_order() -> None:
|
||||
cfg = {"fix_categories": [module.FAILING_ASSERTION, module.LINT_VIOLATION]}
|
||||
assert module.enabled_categories(cfg) == (module.LINT_VIOLATION, module.FAILING_ASSERTION)
|
||||
|
||||
@ -46,6 +46,7 @@ def _code_cfg(**overrides) -> dict: # type: ignore[no-untyped-def]
|
||||
"owner": "bstein",
|
||||
"repo": "hermes-code-demo",
|
||||
"max_open_proposals": 0,
|
||||
"fix_categories": [],
|
||||
"base_branch": "master",
|
||||
"timeout_seconds": 15.0,
|
||||
"legacy_job": JOB,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user