Skip to content

Commit 06f9161

Browse files
committed
fix: include overlapping fragments in core_ids for diff context
When a new function is added at the end of a file, the hunk may start on an empty line before the function definition. Previously, this caused the function fragment to be excluded because it didn't fully cover the hunk. Now we check for overlapping fragments when full coverage fails. Added regression test: test_sel_core_004_new_function_at_end_of_file
1 parent 425aa89 commit 06f9161

5 files changed

Lines changed: 83 additions & 14 deletions

File tree

demo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 4046d4c78425e02b931ecb8cc38d643bbb7c3b7a

src/treemapper/diffctx/__init__.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ def build_diff_context(
7878
if not is_git_repo(root_dir):
7979
raise GitError(f"'{root_dir}' is not a git repository")
8080

81-
if not (0.0 <= alpha < 1.0):
82-
raise ValueError(f"alpha must be in [0, 1), got {alpha}")
81+
if not (0.0 < alpha < 1.0):
82+
raise ValueError(f"alpha must be in (0, 1), got {alpha}")
8383
if tau < 0.0:
8484
raise ValueError(f"tau must be >= 0, got {tau}")
8585

@@ -150,9 +150,14 @@ def build_diff_context(
150150
best = min(covering, key=lambda f: f.line_count)
151151
core_ids.add(best.id)
152152
else:
153-
# Fallback: use enclosing fragment
154-
enc = enclosing_fragment(frags, h_start)
155-
if enc is not None:
153+
# Check for fragments that OVERLAP with the hunk (partial coverage)
154+
overlapping = [f for f in frags if f.start_line <= h_end and f.end_line >= h_start]
155+
if overlapping:
156+
# Add all overlapping fragments as core
157+
for f in overlapping:
158+
core_ids.add(f.id)
159+
elif (enc := enclosing_fragment(frags, h_start)) is not None:
160+
# Fallback: use enclosing fragment
156161
core_ids.add(enc.id)
157162
else:
158163
# For hunks in gaps between fragments, find nearest adjacent fragments

src/treemapper/diffctx/graph.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -622,13 +622,7 @@ def _build_config_code_edges(fragments: list[Fragment]) -> dict[tuple[FragmentId
622622
if not config_frags or not code_frags:
623623
return edges
624624

625-
# Build inverted index: key -> list of code fragments containing the key
626625
key_to_code_frags: dict[str, list[FragmentId]] = defaultdict(list)
627-
for code_frag in code_frags:
628-
content_lower = code_frag.content.lower()
629-
for code_frag_inner in code_frags:
630-
if code_frag_inner.id == code_frag.id:
631-
continue
632626

633627
# Extract keys from config files
634628
config_keys_by_frag: dict[FragmentId, set[str]] = {}

tests/test_diff_edge_cases.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,21 @@ def test_invalid_alpha_negative(self, diff_project):
638638
alpha=-0.1,
639639
)
640640

641+
def test_invalid_alpha_zero(self, diff_project):
642+
"""alpha=0 should raise ValueError (must be in (0, 1))"""
643+
diff_project.add_file("test.py", "x = 1")
644+
diff_project.commit("Initial")
645+
diff_project.add_file("test.py", "x = 2")
646+
diff_project.commit("Modify")
647+
648+
with pytest.raises(ValueError):
649+
build_diff_context(
650+
root_dir=diff_project.repo,
651+
diff_range="HEAD~1..HEAD",
652+
budget_tokens=1000,
653+
alpha=0.0,
654+
)
655+
641656
def test_invalid_alpha_greater_than_one(self, diff_project):
642657
"""alpha >= 1 should raise ValueError"""
643658
diff_project.add_file("test.py", "x = 1")
@@ -669,18 +684,17 @@ def test_invalid_tau_negative(self, diff_project):
669684
)
670685

671686
def test_valid_alpha_boundaries(self, diff_project):
672-
"""alpha=0.0 and alpha=0.99 should work"""
687+
"""alpha=0.01 and alpha=0.99 should work (0 and 1 are excluded)"""
673688
diff_project.add_file("test.py", "x = 1")
674689
diff_project.commit("Initial")
675690
diff_project.add_file("test.py", "x = 2")
676691
diff_project.commit("Modify")
677692

678-
# Should not raise
679693
tree1 = build_diff_context(
680694
root_dir=diff_project.repo,
681695
diff_range="HEAD~1..HEAD",
682696
budget_tokens=1000,
683-
alpha=0.0,
697+
alpha=0.01,
684698
)
685699
assert tree1 is not None
686700

tests/test_diff_selection_regression.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,61 @@ def func2():
158158
assert new_func is not None
159159

160160

161+
def test_sel_core_004_new_function_at_end_of_file(self, diff_project):
162+
"""Regression test: new function added at end of file must be included.
163+
164+
The hunk may start on empty line before function definition,
165+
but the function fragment must still be detected as core.
166+
"""
167+
diff_project.add_file(
168+
"calculator.py",
169+
"""\
170+
def add(a, b):
171+
return a + b
172+
173+
def sub(a, b):
174+
return a - b
175+
176+
def mul(a, b):
177+
return a * b
178+
""",
179+
)
180+
diff_project.commit("Initial calculator")
181+
182+
diff_project.add_file(
183+
"calculator.py",
184+
"""\
185+
def add(a, b):
186+
return a + b
187+
188+
def sub(a, b):
189+
return a - b
190+
191+
def mul(a, b):
192+
return a * b
193+
194+
def div(a, b):
195+
if b == 0:
196+
raise ValueError("division by zero")
197+
return a / b
198+
""",
199+
)
200+
diff_project.commit("Add div function")
201+
202+
tree = build_diff_context(
203+
root_dir=diff_project.repo,
204+
diff_range="HEAD~1..HEAD",
205+
budget_tokens=10000,
206+
)
207+
208+
fragments = _extract_fragments_from_tree(tree)
209+
div_func = next((f for f in fragments if "def div" in f.get("content", "")), None)
210+
assert div_func is not None, "div function must be included in diff context"
211+
212+
mul_func = next((f for f in fragments if "def mul" in f.get("content", "")), None)
213+
assert mul_func is None, "mul function should NOT be included (not changed)"
214+
215+
161216
class TestBudgetBehavior:
162217
def test_sel_budget_001_respects_token_limit(self):
163218
fragments = [

0 commit comments

Comments
 (0)