From 3de2dd0df65bcf5ac0f14ffd1868abac7ac7191c Mon Sep 17 00:00:00 2001 From: vitali87 Date: Sun, 30 Aug 2026 14:24:55 +0100 Subject: [PATCH] fix: list per-file global hunk indices in chunk prompt summaries The chunk file summary said 'indices 0..N-1' where N was the number of that file's hunks in the chunk, while the diff labels (and the system prompt's instructions) use per-file global indices. For a file split across chunks the summary told the model to use indices already assigned in an earlier chunk. FileSummary now carries the actual indices and the summary lists them when they are not 0..N-1. --- pr_split/diff_ops/parser.py | 1 + pr_split/planner/chunker.py | 1 + pr_split/planner/prompts.py | 12 +++++++++++- pr_split/types_defs.py | 1 + tests/test_chunker.py | 19 +++++++++++++++++++ tests/test_prompts.py | 28 ++++++++++++++++++++++++++++ tests/test_types_defs.py | 1 + 7 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pr_split/diff_ops/parser.py b/pr_split/diff_ops/parser.py index 850ccdc..80e8dc8 100644 --- a/pr_split/diff_ops/parser.py +++ b/pr_split/diff_ops/parser.py @@ -60,6 +60,7 @@ def stats(self) -> DiffStats: is_deleted=pf.is_removed_file, is_renamed=pf.is_rename, hunk_count=len(pf), + hunk_indices=list(range(len(pf))), ) ) return DiffStats( diff --git a/pr_split/planner/chunker.py b/pr_split/planner/chunker.py index 96e3d52..c36c917 100644 --- a/pr_split/planner/chunker.py +++ b/pr_split/planner/chunker.py @@ -184,6 +184,7 @@ def build_chunk_stats_from_hunks(parsed_diff: ParsedDiff, hunk_refs: list[HunkRe is_deleted=pf.is_removed_file, is_renamed=pf.is_rename, hunk_count=len(indices), + hunk_indices=sorted(indices), ) ) return DiffStats( diff --git a/pr_split/planner/prompts.py b/pr_split/planner/prompts.py index 0e75283..d6a3dc3 100644 --- a/pr_split/planner/prompts.py +++ b/pr_split/planner/prompts.py @@ -209,7 +209,17 @@ def _format_file_summary(diff_stats: DiffStats) -> str: ] flag_str = f" [{', '.join(flags)}]" if flags else "" hunk_count = fs["hunk_count"] - idx_range = f"indices 0..{hunk_count - 1}" if hunk_count > 0 else "no hunks" + indices = fs["hunk_indices"] + # In chunked mode a file's hunks in this chunk keep their per-file + # global indices, which need not start at 0 or be contiguous; list + # them explicitly so the model does not re-use indices from earlier + # chunks. + if not indices: + idx_range = "no hunks" + elif indices == list(range(len(indices))): + idx_range = f"indices 0..{indices[-1]}" + else: + idx_range = f"indices {', '.join(str(i) for i in indices)} (this chunk only)" lines.append( f" {fs['path']}: +{fs['added']}/-{fs['removed']}" f" ({hunk_count} hunks, {idx_range}){flag_str}" diff --git a/pr_split/types_defs.py b/pr_split/types_defs.py index aa61a66..a4f3134 100644 --- a/pr_split/types_defs.py +++ b/pr_split/types_defs.py @@ -23,6 +23,7 @@ class FileSummary(TypedDict): is_deleted: bool is_renamed: bool hunk_count: int + hunk_indices: list[int] class ForkPRInfo(TypedDict): diff --git a/tests/test_chunker.py b/tests/test_chunker.py index 8af1b00..be804f8 100644 --- a/tests/test_chunker.py +++ b/tests/test_chunker.py @@ -330,3 +330,22 @@ def test_includes_file_paths(self) -> None: def test_empty_groups(self) -> None: result = format_group_catalog([]) assert result == "" + + +class TestChunkStatsKeepGlobalIndices: + def test_second_chunk_summary_uses_per_file_global_indices(self) -> None: + from pr_split.diff_ops.parser import parse_diff + from pr_split.planner.chunker import build_chunk_stats_from_hunks, build_hunk_sequence + from pr_split.planner.prompts import build_chunk_continuation_prompt + + diff = "diff --git a/f.py b/f.py\n--- a/f.py\n+++ b/f.py\n" + "".join( + f"@@ -{10 * i + 1},1 +{10 * i + 1},2 @@\n x\n+y{i}\n" for i in range(4) + ) + parsed = parse_diff(diff) + refs = build_hunk_sequence(parsed, 0.25) + second_chunk = refs[2:] + stats = build_chunk_stats_from_hunks(parsed, second_chunk) + assert stats["file_summaries"][0]["hunk_indices"] == [2, 3] + prompt = build_chunk_continuation_prompt(stats, "diff", 2, 2, "catalog") + assert "indices 2, 3 (this chunk only)" in prompt + assert "indices 0..1" not in prompt diff --git a/tests/test_prompts.py b/tests/test_prompts.py index 6fb6d3a..297fd34 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -27,6 +27,7 @@ def _make_diff_stats() -> DiffStats: is_deleted=False, is_renamed=False, hunk_count=2, + hunk_indices=[0, 1], ), FileSummary( path="bar.py", @@ -36,6 +37,7 @@ def _make_diff_stats() -> DiffStats: is_deleted=False, is_renamed=False, hunk_count=1, + hunk_indices=[0], ), ], ) @@ -101,6 +103,7 @@ def test_zero_hunk_file(self) -> None: is_deleted=False, is_renamed=False, hunk_count=0, + hunk_indices=[], ), ], ) @@ -130,3 +133,28 @@ class TestBuildSystemPromptExtended: def test_returns_nonempty_string(self) -> None: result = build_system_prompt(Priority.ORTHOGONAL, 400) assert len(result) > 100 + + +class TestChunkSummaryIndices: + def test_later_chunk_lists_global_indices(self) -> None: + stats = DiffStats( + total_files=1, + total_added=1, + total_removed=0, + total_loc=1, + file_summaries=[ + FileSummary( + path="f.py", + added=1, + removed=0, + is_new=False, + is_deleted=False, + is_renamed=False, + hunk_count=2, + hunk_indices=[1, 3], + ) + ], + ) + result = build_user_prompt(stats, "diff") + assert "indices 1, 3 (this chunk only)" in result + assert "indices 0..1" not in result diff --git a/tests/test_types_defs.py b/tests/test_types_defs.py index 9bce6b2..797b3ca 100644 --- a/tests/test_types_defs.py +++ b/tests/test_types_defs.py @@ -55,6 +55,7 @@ def test_dict_access(self) -> None: "is_deleted": False, "is_renamed": False, "hunk_count": 2, + "hunk_indices": [0, 1], } assert fs["path"] == "test.py" assert fs["added"] == 10