From d876c7a6fba0486522cd9ea50e8ef300b9eca39c Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 7 Aug 2026 05:22:03 -0300 Subject: [PATCH] fix(hermes): stop describing a finding as a failed build The first advice issues went out rendered through build-failure wording and were wrong in four places at once: the title printed a source line as a build number, the header claimed a first failed gate for a build that passed, the heading asked why a human was needed when nothing had broken, and the SonarQube link was labelled "Failed build". Every one of those is small and every one of them is the kind of thing that makes a reader distrust the rest of the page - which matters more here than usual, because the whole point of the issue is a suggestion they have to judge for themselves. A finding-sourced issue now says what it is: SonarQube reports this rule in this file, the build is green, this is a standing finding rather than a failure. Build-failure issues are untouched, and a test asserts each wording stays out of the other. Co-Authored-By: Claude Opus 5 --- ariadne/services/hermes_incident_body.py | 39 ++++++++++++++--- ariadne/services/hermes_sonar_advice.py | 1 + tests/test_hermes_sonar_advice.py | 55 ++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/ariadne/services/hermes_incident_body.py b/ariadne/services/hermes_incident_body.py index 05d6c89..163fd0e 100644 --- a/ariadne/services/hermes_incident_body.py +++ b/ariadne/services/hermes_incident_body.py @@ -22,6 +22,17 @@ from ariadne.services import hermes_suggested_remediation as suggestion_field DEFAULT_MAX_BODY_CHARS = 8000 UNDIAGNOSED = "undiagnosed" +# A finding is not a build failure, and rendering one through the other's +# wording produces an issue that is wrong in every heading: a line number +# printed as a build, a "first failed gate" for a build that passed, and a +# SonarQube link labelled "failed build". +SONAR_PREFIX = "sonar/" + + +def is_finding(context: dict) -> bool: + """Report whether this issue describes a finding rather than a failure.""" + + return str(context.get("incident_id") or "").startswith(SONAR_PREFIX) _MAX_TITLE_CHARS = 120 _MAX_FACTS = 10 @@ -94,7 +105,10 @@ def issue_title(context: dict) -> str: `[hermes] {job} #{build}: {classification}` clipped to 120 characters. """ - prefix = f"[hermes] {context.get('job')} #{context.get('build_number')}: " + if is_finding(context): + prefix = f"[hermes] {context.get('job')}: " + else: + prefix = f"[hermes] {context.get('job')} #{context.get('build_number')}: " classification = str(context.get("classification") or UNDIAGNOSED) room = _MAX_TITLE_CHARS - len(prefix) if room <= len(_ELLIPSIS): @@ -149,6 +163,11 @@ def _summary_line(context: dict) -> str: incident_id = context.get("incident_id") classification = context.get("classification") + if is_finding(context): + return ( + f"SonarQube reports **{classification}** in `{context.get('finding_path')}`. " + "The build is green; this is a standing finding, not a failure." + ) run_id = str(context.get("run_id") or "") if not run_id or run_id == "unknown": # The headline is the first thing read, so it must not credit a model @@ -178,14 +197,17 @@ def _footer(context: dict) -> str: def _human_section(context: dict) -> str: """Render the section explaining why the incident needs a person.""" - lines = ["## Why a human is needed", str(context.get("reason") or "no reason recorded")] + heading = "## What is wrong" if is_finding(context) else "## Why a human is needed" + lines = [heading, str(context.get("reason") or "no reason recorded")] observation = str(context.get("observation") or "").strip() if observation: # Escalations that never reached a model carry their explanation here # rather than in facts, which are only populated from a diagnosis. lines.append(observation) authorize_reason = str(context.get("authorize_reason") or "") - if authorize_reason: + if authorize_reason and is_finding(context): + lines.append(f"Ariadne opened no pull request for it: {authorize_reason}.") + elif authorize_reason: lines.append(f"Ariadne did not authorize automated remediation: `{authorize_reason}`.") return "\n\n".join(lines) @@ -220,10 +242,13 @@ def _links_section(context: dict) -> str: build_url = str(context.get("build_url") or "") proposal_url = str(context.get("code_proposal_url") or "") - lines = [ - "## Links", - f"- Failed build: {build_url}" if build_url else "- Failed build: url unavailable", - ] + if is_finding(context): + lines = ["## Links", f"- SonarQube finding: {build_url}" if build_url else "- SonarQube finding: url unavailable"] + else: + lines = [ + "## Links", + f"- Failed build: {build_url}" if build_url else "- Failed build: url unavailable", + ] if proposal_url: lines.append(f"- Proposed fix awaiting review: {proposal_url}") lines.append(f"- {_AUDIT_NOTE}") diff --git a/ariadne/services/hermes_sonar_advice.py b/ariadne/services/hermes_sonar_advice.py index ae44c07..ba5e14e 100644 --- a/ariadne/services/hermes_sonar_advice.py +++ b/ariadne/services/hermes_sonar_advice.py @@ -209,6 +209,7 @@ def _context( # noqa: PLR0913 - the issue body needs every one of these "classification": rule, "confidence": None, "first_failed_gate": "", + "finding_path": issue.get("path"), "reason": reason, # Says plainly why nobody patched it, so the issue does not read as a # failure of the automation. diff --git a/tests/test_hermes_sonar_advice.py b/tests/test_hermes_sonar_advice.py index 93faece..90a4a84 100644 --- a/tests/test_hermes_sonar_advice.py +++ b/tests/test_hermes_sonar_advice.py @@ -232,3 +232,58 @@ def test_the_prompt_bounds_the_file_it_sends() -> None: prompt = module._prompt(INCIDENT, dict(ISSUE), "x" * 90000) assert len(prompt) < 45000 + + +def test_a_finding_issue_never_uses_build_failure_wording() -> None: + """A line number printed as a build number reads as a failure that never happened.""" + + from ariadne.services import hermes_incident_body as body + from ariadne.services import hermes_code_suggestion as cs + + context = { + "incident_id": INCIDENT, + "job": "ariadne", + "build_number": 42, + "classification": "python:S3776", + "finding_path": "ariadne/services/thing.py", + "reason": "The function branches five ways.", + "authorize_reason": "no automated patch was possible for this finding", + "build_url": "https://quality.example/project/issues?open=AZ1", + "run_id": "run_x", + "run_url": "https://agent.example/chat?resume=run_x", + "code_suggestions": cs.from_payload({"code_suggestions": [dict(SUGGESTION)]}), + } + + assert body.issue_title(context) == "[hermes] ariadne: python:S3776" + rendered = body.issue_body(context) + assert rendered.startswith("SonarQube reports **python:S3776**") + assert "The build is green" in rendered + assert "## What is wrong" in rendered + assert "- SonarQube finding: https://quality.example" in rendered + assert "Failed build" not in rendered + assert "first failed gate" not in rendered + assert "Why a human is needed" not in rendered + + +def test_a_build_failure_issue_is_unchanged() -> None: + """The finding wording must not leak into real triage issues.""" + + from ariadne.services import hermes_incident_body as body + + context = { + "incident_id": "ariadne/408", + "job": "ariadne", + "build_number": 408, + "classification": "pytest_test_failure", + "reason": "a repository test failure", + "authorize_reason": "human_required", + "build_url": "https://ci.example/job/ariadne/408/", + "run_id": "run_y", + } + + assert body.issue_title(context) == "[hermes] ariadne #408: pytest_test_failure" + rendered = body.issue_body(context) + assert "## Why a human is needed" in rendered + assert "- Failed build: https://ci.example" in rendered + assert "did not authorize automated remediation" in rendered + assert "SonarQube" not in rendered