mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 07:52:14 +00:00
feat(open-swe): always anchor findings to a single line (#1264)
* feat(open-swe): always anchor findings to a single line GitHub renders multi-line review comment ranges as walls of context above the comment, which buries the point. Match Devin Review's behavior and always collapse `end_line` to `start_line` so each finding is anchored to the single most relevant line. Removes the (now unused) `MAX_FINDING_RANGE_LINES` cap and `clip_finding_range` helper. * fix(open-swe): collapse end_line before diff-range check
This commit is contained in:
parent
089db06135
commit
bdea1fb6da
4 changed files with 22 additions and 80 deletions
|
|
@ -109,12 +109,11 @@ Cloning is optional — for most PRs the diff alone is enough.
|
||||||
issue worth surfacing; `low` = small nit; `informational` = FYI / context,
|
issue worth surfacing; `low` = small nit; `informational` = FYI / context,
|
||||||
not a flaw. Inflated severities erode trust — be honest.
|
not a flaw. Inflated severities erode trust — be honest.
|
||||||
- `category`: e.g. `correctness`, `security`, `perf`, `style`, `flag`.
|
- `category`: e.g. `correctness`, `security`, `perf`, `style`, `flag`.
|
||||||
- `file`, `start_line`, `end_line`: anchor inside the PR diff. Keep
|
- `file`, `start_line`: anchor the comment to a single line inside the
|
||||||
ranges tight — anchor to the few lines that actually matter (the call
|
PR diff — the call site, the signature line, the conditional that's
|
||||||
site, the signature line, the conditional). **Do not range-select an
|
actually wrong. The tool always anchors to one line because GitHub
|
||||||
entire function**; GitHub renders the full range as context above the
|
renders multi-line ranges as walls of context that bury the comment.
|
||||||
comment and buries the point. Ranges over ~10 lines are auto-collapsed
|
`end_line` is accepted for API compatibility but ignored.
|
||||||
to the start line.
|
|
||||||
- `description`: what's wrong, in 1–4 sentences. Markdown is fine.
|
- `description`: what's wrong, in 1–4 sentences. Markdown is fine.
|
||||||
- `suggestion`: **only** include for small, obvious fixes that fit in 4
|
- `suggestion`: **only** include for small, obvious fixes that fit in 4
|
||||||
lines or fewer — a one-liner rename, a missing guard, a typo, a flipped
|
lines or fewer — a one-liner rename, a missing guard, a typo, a flipped
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,6 @@ REVIEWER_THREAD_KIND = "reviewer"
|
||||||
# longer suggestions; the description still gets posted on its own.
|
# longer suggestions; the description still gets posted on its own.
|
||||||
MAX_SUGGESTION_LINES = 4
|
MAX_SUGGESTION_LINES = 4
|
||||||
|
|
||||||
# Anchoring a finding to a giant range (e.g. an entire function) makes the
|
|
||||||
# GitHub comment dump dozens of lines of context above the actual review
|
|
||||||
# text, which buries the point. When the range exceeds this, collapse it to
|
|
||||||
# the first line — the comment still anchors near the issue without showing
|
|
||||||
# the whole block. ~5-line ranges are still useful, so the cap is generous.
|
|
||||||
MAX_FINDING_RANGE_LINES = 10
|
|
||||||
|
|
||||||
|
|
||||||
def clip_suggestion(suggestion: str | None) -> tuple[str | None, bool]:
|
def clip_suggestion(suggestion: str | None) -> tuple[str | None, bool]:
|
||||||
"""Return (suggestion_or_none, was_dropped). Drops if over the line cap."""
|
"""Return (suggestion_or_none, was_dropped). Drops if over the line cap."""
|
||||||
|
|
@ -46,18 +39,6 @@ def clip_suggestion(suggestion: str | None) -> tuple[str | None, bool]:
|
||||||
return suggestion, False
|
return suggestion, False
|
||||||
|
|
||||||
|
|
||||||
def clip_finding_range(
|
|
||||||
start_line: int | None,
|
|
||||||
end_line: int | None,
|
|
||||||
) -> tuple[int | None, int | None, bool]:
|
|
||||||
"""Return (start, end, was_collapsed). Collapses end→start if over cap."""
|
|
||||||
if start_line is None or end_line is None:
|
|
||||||
return start_line, end_line, False
|
|
||||||
if end_line - start_line + 1 > MAX_FINDING_RANGE_LINES:
|
|
||||||
return start_line, start_line, True
|
|
||||||
return start_line, end_line, False
|
|
||||||
|
|
||||||
|
|
||||||
Severity = Literal["informational", "low", "medium", "high", "critical"]
|
Severity = Literal["informational", "low", "medium", "high", "critical"]
|
||||||
FindingStatus = Literal["open", "resolved", "dismissed"]
|
FindingStatus = Literal["open", "resolved", "dismissed"]
|
||||||
DiffSide = Literal["LEFT", "RIGHT"]
|
DiffSide = Literal["LEFT", "RIGHT"]
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,11 @@ from langgraph.config import get_config
|
||||||
|
|
||||||
from ..reviewer_diff import is_range_in_diff
|
from ..reviewer_diff import is_range_in_diff
|
||||||
from ..reviewer_findings import (
|
from ..reviewer_findings import (
|
||||||
MAX_FINDING_RANGE_LINES,
|
|
||||||
MAX_SUGGESTION_LINES,
|
MAX_SUGGESTION_LINES,
|
||||||
DiffSide,
|
DiffSide,
|
||||||
Finding,
|
Finding,
|
||||||
Severity,
|
Severity,
|
||||||
append_finding,
|
append_finding,
|
||||||
clip_finding_range,
|
|
||||||
clip_suggestion,
|
clip_suggestion,
|
||||||
get_thread_id_from_runtime,
|
get_thread_id_from_runtime,
|
||||||
new_finding,
|
new_finding,
|
||||||
|
|
@ -54,15 +52,14 @@ def add_finding(
|
||||||
``style``, ``flag``, etc.). Free-form; used for grouping in the UI.
|
``style``, ``flag``, etc.). Free-form; used for grouping in the UI.
|
||||||
file: Repo-relative path of the file the finding refers to.
|
file: Repo-relative path of the file the finding refers to.
|
||||||
description: Markdown body the user sees.
|
description: Markdown body the user sees.
|
||||||
start_line: 1-based start line in the new (post-PR) file. Equal to
|
start_line: 1-based line in the new (post-PR) file to anchor the
|
||||||
``end_line`` for single-line findings; less than ``end_line`` for
|
comment to. Pick the single most relevant line — the call site,
|
||||||
ranges. Omit (with ``end_line``) for file-level findings.
|
the signature, or the line the issue is actually about. Omit
|
||||||
end_line: 1-based end line, inclusive. Defaults to ``start_line``.
|
(with ``end_line``) for file-level findings.
|
||||||
Keep ranges tight — anchor to the few lines that actually matter
|
end_line: Accepted for API compatibility, but findings are always
|
||||||
for the comment. Ranges over 10 lines are auto-collapsed to a
|
anchored to a single line (``start_line``). GitHub renders
|
||||||
single line because GitHub renders the full range as context
|
multi-line ranges as walls of context that bury the comment, so
|
||||||
above the comment, which buries the point. Don't anchor to an
|
we collapse to one line for a cleaner comment.
|
||||||
entire function; pick the call site or signature line instead.
|
|
||||||
suggestion: Replacement text for ``start_line..end_line``. When set,
|
suggestion: Replacement text for ``start_line..end_line``. When set,
|
||||||
the published GitHub comment includes a ```suggestion``` block so
|
the published GitHub comment includes a ```suggestion``` block so
|
||||||
the user can click "Commit suggestion". **Only set this for small,
|
the user can click "Commit suggestion". **Only set this for small,
|
||||||
|
|
@ -89,6 +86,9 @@ def add_finding(
|
||||||
if start_line is not None and end_line is not None and end_line < start_line:
|
if start_line is not None and end_line is not None and end_line < start_line:
|
||||||
return {"success": False, "error": "end_line must be >= start_line"}
|
return {"success": False, "error": "end_line must be >= start_line"}
|
||||||
|
|
||||||
|
if start_line is not None:
|
||||||
|
end_line = start_line
|
||||||
|
|
||||||
config = get_config()
|
config = get_config()
|
||||||
configurable = config.get("configurable", {}) if isinstance(config, dict) else {}
|
configurable = config.get("configurable", {}) if isinstance(config, dict) else {}
|
||||||
diff_line_set = configurable.get("diff_line_set") if isinstance(configurable, dict) else None
|
diff_line_set = configurable.get("diff_line_set") if isinstance(configurable, dict) else None
|
||||||
|
|
@ -106,8 +106,6 @@ def add_finding(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
start_line, end_line, range_collapsed = clip_finding_range(start_line, end_line)
|
|
||||||
|
|
||||||
diff_hunk: str | None = None
|
diff_hunk: str | None = None
|
||||||
if isinstance(diff_text, str) and diff_text:
|
if isinstance(diff_text, str) and diff_text:
|
||||||
from ..reviewer_diff import extract_diff_hunk
|
from ..reviewer_diff import extract_diff_hunk
|
||||||
|
|
@ -139,14 +137,6 @@ def add_finding(
|
||||||
"dropped — the finding was recorded with description only. Only "
|
"dropped — the finding was recorded with description only. Only "
|
||||||
"include `suggestion` for small, obvious fixes."
|
"include `suggestion` for small, obvious fixes."
|
||||||
)
|
)
|
||||||
if range_collapsed:
|
|
||||||
result["range_collapsed"] = True
|
|
||||||
result["range_warning"] = (
|
|
||||||
f"Range exceeded the {MAX_FINDING_RANGE_LINES}-line cap and was "
|
|
||||||
f"collapsed to a single line ({start_line}). Anchor findings to "
|
|
||||||
"the most relevant line — GitHub renders large ranges as walls "
|
|
||||||
"of context that bury the comment."
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ def test_add_finding_persists_to_thread_metadata() -> None:
|
||||||
assert persisted_thread == "tid-1"
|
assert persisted_thread == "tid-1"
|
||||||
assert persisted["file"] == "foo.py"
|
assert persisted["file"] == "foo.py"
|
||||||
assert persisted["start_line"] == 11
|
assert persisted["start_line"] == 11
|
||||||
assert persisted["end_line"] == 12
|
assert persisted["end_line"] == 11
|
||||||
assert persisted["suggestion"] == "renamed = 1"
|
assert persisted["suggestion"] == "renamed = 1"
|
||||||
assert persisted["status"] == "open"
|
assert persisted["status"] == "open"
|
||||||
assert persisted["first_seen_sha"] == "sha-head"
|
assert persisted["first_seen_sha"] == "sha-head"
|
||||||
|
|
@ -175,35 +175,8 @@ def test_add_finding_keeps_short_suggestion() -> None:
|
||||||
assert captured[0]["suggestion"] == short_suggestion
|
assert captured[0]["suggestion"] == short_suggestion
|
||||||
|
|
||||||
|
|
||||||
def test_add_finding_collapses_oversized_range() -> None:
|
def test_add_finding_always_collapses_to_single_line() -> None:
|
||||||
captured: list[Any] = []
|
"""Multi-line ranges are always collapsed to ``start_line``."""
|
||||||
|
|
||||||
async def fake_append(thread_id: str, finding: Any) -> Any:
|
|
||||||
captured.append(finding)
|
|
||||||
return finding
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("agent.tools.add_finding.get_config", return_value=_config()),
|
|
||||||
patch("agent.tools.add_finding.get_thread_id_from_runtime", return_value="tid-1"),
|
|
||||||
patch("agent.tools.add_finding.append_finding", side_effect=fake_append),
|
|
||||||
):
|
|
||||||
result = add_finding(
|
|
||||||
severity="medium",
|
|
||||||
category="correctness",
|
|
||||||
file="foo.py",
|
|
||||||
description="big function",
|
|
||||||
start_line=15,
|
|
||||||
end_line=40, # 26 lines — well over the cap
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["success"] is True
|
|
||||||
assert result.get("range_collapsed") is True
|
|
||||||
assert "range_warning" in result
|
|
||||||
assert captured[0]["start_line"] == 15
|
|
||||||
assert captured[0]["end_line"] == 15
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_finding_keeps_small_range() -> None:
|
|
||||||
captured: list[Any] = []
|
captured: list[Any] = []
|
||||||
|
|
||||||
async def fake_append(thread_id: str, finding: Any) -> Any:
|
async def fake_append(thread_id: str, finding: Any) -> Any:
|
||||||
|
|
@ -219,15 +192,14 @@ def test_add_finding_keeps_small_range() -> None:
|
||||||
severity="low",
|
severity="low",
|
||||||
category="style",
|
category="style",
|
||||||
file="foo.py",
|
file="foo.py",
|
||||||
description="five lines",
|
description="anchor on start_line",
|
||||||
start_line=15,
|
start_line=15,
|
||||||
end_line=19, # 5 lines — within the cap
|
end_line=19,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert "range_collapsed" not in result
|
|
||||||
assert captured[0]["start_line"] == 15
|
assert captured[0]["start_line"] == 15
|
||||||
assert captured[0]["end_line"] == 19
|
assert captured[0]["end_line"] == 15
|
||||||
|
|
||||||
|
|
||||||
def test_update_finding_rejects_long_suggestion_without_clobbering() -> None:
|
def test_update_finding_rejects_long_suggestion_without_clobbering() -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue