From dc98609141258e7ba1149230764215dfdeed9f6f Mon Sep 17 00:00:00 2001 From: shobhitagnihotri69 Date: Thu, 17 Sep 2026 08:12:54 +0530 Subject: [PATCH 1/2] fix(reward): ignore git extended headers and mode lines in diff normalization - Ignore git extended headers (new/deleted file mode, old/new mode, similarity index, copy/rename metadata) in _normalize_diff - Ignore '\ No newline at end of file' diff markers - Prevent false reward degradation when predicted patch matches oracle code changes but differs in permission bits or file modes - Add unit test verifying identical code changes with extended headers score 1.0 --- src/repo2rlenv/reward.py | 8 ++++++++ tests/test_reward.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/repo2rlenv/reward.py b/src/repo2rlenv/reward.py index 2caa3fe2..44ada1d9 100644 --- a/src/repo2rlenv/reward.py +++ b/src/repo2rlenv/reward.py @@ -31,6 +31,10 @@ _FILE_HEADER_RE = re.compile(r"^(?:---|\+\+\+) ") _INDEX_LINE_RE = re.compile(r"^index ") _DIFF_GIT_RE = re.compile(r"^diff --git ") +_GIT_EXTENDED_HEADER_RE = re.compile( + r"^(?:new file mode|deleted file mode|old mode|new mode|similarity index|copy from|copy to|rename from|rename to|dissimilarity index)\b" +) +_NO_NEWLINE_RE = re.compile(r"^\\ No newline at end of file") @dataclass(slots=True) @@ -50,6 +54,10 @@ def _normalize_diff(diff: str) -> list[str]: continue if _INDEX_LINE_RE.match(line): continue + if _GIT_EXTENDED_HEADER_RE.match(line): + continue + if _NO_NEWLINE_RE.match(line): + continue if _HUNK_HEADER_RE.match(line): lines.append("@@") # keep as a separator but drop line numbers continue diff --git a/tests/test_reward.py b/tests/test_reward.py index d2a5c1fc..a3a7b79a 100644 --- a/tests/test_reward.py +++ b/tests/test_reward.py @@ -77,3 +77,29 @@ def test_partial_match_scores_in_between(): b = SAMPLE_DIFF.replace("hello, world", "goodbye") reward, _ = calculate_diff_similarity_reward(a, b) assert 0.5 < reward < 1.0 + + +def test_normalization_ignores_git_extended_headers_and_mode_changes(): + """Diffs that differ only in file modes or extended git headers should score 1.0.""" + oracle = """diff --git a/script.py b/script.py +new file mode 100755 +index 0000000..abcdef1 +--- /dev/null ++++ b/script.py +@@ -0,0 +1,2 @@ ++#!/usr/bin/env python3 ++print("run") +\\ No newline at end of file +""" + predicted = """diff --git a/script.py b/script.py +index 0000000..abcdef1 +--- /dev/null ++++ b/script.py +@@ -0,0 +1,2 @@ ++#!/usr/bin/env python3 ++print("run") +""" + reward, meta = calculate_diff_similarity_reward(oracle, predicted) + assert reward == 1.0 + assert meta.parse_error is None + From 32b297affb9d95d0c1b3a3034d466dffb3a60201 Mon Sep 17 00:00:00 2001 From: shobhitagnihotri69 Date: Thu, 17 Sep 2026 14:26:10 +0530 Subject: [PATCH 2/2] fix(reward): preserve mode and rename lines when diff has no content changes - Update _normalize_diff to preserve mode-only and rename-only changes - Retain git extended headers and file path headers when no hunks are present so metadata-only patches are not normalized to empty diffs - Add regression tests for identical mode-only, mode mismatch, and rename-only patches - Fix ruff formatting in tests/test_reward.py --- src/repo2rlenv/reward.py | 69 ++++++++++++++++++++++++++++------------ tests/test_reward.py | 58 +++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/src/repo2rlenv/reward.py b/src/repo2rlenv/reward.py index 44ada1d9..f0bb267c 100644 --- a/src/repo2rlenv/reward.py +++ b/src/repo2rlenv/reward.py @@ -31,8 +31,9 @@ _FILE_HEADER_RE = re.compile(r"^(?:---|\+\+\+) ") _INDEX_LINE_RE = re.compile(r"^index ") _DIFF_GIT_RE = re.compile(r"^diff --git ") -_GIT_EXTENDED_HEADER_RE = re.compile( - r"^(?:new file mode|deleted file mode|old mode|new mode|similarity index|copy from|copy to|rename from|rename to|dissimilarity index)\b" +_SIMILARITY_LINE_RE = re.compile(r"^(?:similarity index|dissimilarity index)\b") +_MODE_OR_RENAME_RE = re.compile( + r"^(?:old mode|new mode|new file mode|deleted file mode|copy from|copy to|rename from|rename to)\b" ) _NO_NEWLINE_RE = re.compile(r"^\\ No newline at end of file") @@ -46,26 +47,54 @@ class DiffRewardMetadata: parse_error: str | None = None -def _normalize_diff(diff: str) -> list[str]: - """Strip volatile metadata (hunk line numbers, indices, file headers context).""" - lines: list[str] = [] +def _split_diff_sections(diff: str) -> list[list[str]]: + sections: list[list[str]] = [] + current: list[str] = [] for line in diff.splitlines(): if _DIFF_GIT_RE.match(line): - continue - if _INDEX_LINE_RE.match(line): - continue - if _GIT_EXTENDED_HEADER_RE.match(line): - continue - if _NO_NEWLINE_RE.match(line): - continue - if _HUNK_HEADER_RE.match(line): - lines.append("@@") # keep as a separator but drop line numbers - continue - if _FILE_HEADER_RE.match(line): - # Keep filename markers but normalize whitespace - lines.append(line.split("\t")[0].strip()) - continue - lines.append(line) + if current: + sections.append(current) + current = [line] + else: + current.append(line) + if current: + sections.append(current) + return sections + + +def _normalize_diff(diff: str) -> list[str]: + """Strip volatile metadata while preserving mode-only and rename-only changes.""" + lines: list[str] = [] + for section in _split_diff_sections(diff): + has_hunks = any(_HUNK_HEADER_RE.match(line) for line in section) + has_file_headers = any(_FILE_HEADER_RE.match(line) for line in section) + + for line in section: + if _DIFF_GIT_RE.match(line): + # Keep diff --git if there are no ---/+++ markers (e.g. mode-only or rename-only changes) + if not has_file_headers: + lines.append(line.strip()) + continue + if _INDEX_LINE_RE.match(line): + continue + if _SIMILARITY_LINE_RE.match(line): + continue + if _NO_NEWLINE_RE.match(line): + continue + if _MODE_OR_RENAME_RE.match(line): + # Extended headers are volatile when content hunks are present, + # but essential to preserve when there are mode-only or rename-only changes. + if not has_hunks: + lines.append(line.strip()) + continue + if _HUNK_HEADER_RE.match(line): + lines.append("@@") # keep as a separator but drop line numbers + continue + if _FILE_HEADER_RE.match(line): + # Keep filename markers but normalize whitespace + lines.append(line.split("\t")[0].strip()) + continue + lines.append(line) return lines diff --git a/tests/test_reward.py b/tests/test_reward.py index a3a7b79a..067813ee 100644 --- a/tests/test_reward.py +++ b/tests/test_reward.py @@ -103,3 +103,61 @@ def test_normalization_ignores_git_extended_headers_and_mode_changes(): assert reward == 1.0 assert meta.parse_error is None + +def test_mode_only_patch_scores_one(): + """An identical mode-only patch (e.g. 100644 to 100755) should score 1.0.""" + diff = """diff --git a/script.sh b/script.sh +old mode 100644 +new mode 100755 +""" + reward, meta = calculate_diff_similarity_reward(diff, diff) + assert reward == 1.0 + assert meta.parse_error is None + + +def test_mode_only_patch_mismatch(): + """A mode-only patch with differing target mode should score less than 1.0.""" + oracle = """diff --git a/script.sh b/script.sh +old mode 100644 +new mode 100755 +""" + predicted = """diff --git a/script.sh b/script.sh +old mode 100644 +new mode 100644 +""" + reward, _ = calculate_diff_similarity_reward(oracle, predicted) + assert reward < 1.0 + + +def test_rename_only_patch_scores_one(): + """An identical rename-only patch should score 1.0.""" + oracle = """diff --git a/old.py b/new.py +similarity index 100% +rename from old.py +rename to new.py +""" + predicted = """diff --git a/old.py b/new.py +similarity index 100% +rename from old.py +rename to new.py +""" + reward, meta = calculate_diff_similarity_reward(oracle, predicted) + assert reward == 1.0 + assert meta.parse_error is None + + +def test_multi_file_diff_with_mode_only_change(): + """Diff with both code hunks and mode-only changes should score 1.0 when identical.""" + diff = """diff --git a/foo.py b/foo.py +--- a/foo.py ++++ b/foo.py +@@ -1,2 +1,2 @@ +-print(1) ++print(2) +diff --git a/run.sh b/run.sh +old mode 100644 +new mode 100755 +""" + reward, meta = calculate_diff_similarity_reward(diff, diff) + assert reward == 1.0 + assert meta.parse_error is None