Skip to content

Commit 8623328

Browse files
committed
update
1 parent c2450ab commit 8623328

4 files changed

Lines changed: 219 additions & 27 deletions

File tree

.claude/skills/issue-to-pr/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ For `[Rule]` issues, `ISSUE_JSON` already includes `source_problem`, `target_pro
7676

7777
### 4. Research References
7878

79-
Use `WebSearch` and `WebFetch` to look up the reference URL provided in the issue. This helps:
79+
Use web search to look up the reference URL provided in the issue. This helps:
8080
- Clarify the formal problem definition and notation
8181
- Understand the reduction algorithm in detail (variable mapping, penalty terms, proof of correctness)
8282
- Resolve any ambiguities in the issue description without bothering the contributor
@@ -104,8 +104,8 @@ Include the concrete details from the issue (problem definition, reduction algor
104104
- Otherwise, ensure the information provided is enough to implement a solver.
105105

106106
**Example rules:**
107-
- Implement the user-provided example instance as an example program in `examples/`.
108-
- Run the example; verify JSON output against user-provided information.
107+
- Implement the user-provided example instance in the canonical `example_db` path for the issue (`src/example_db/model_builders.rs` or `src/example_db/rule_builders.rs`, as appropriate).
108+
- Run the relevant export and fixture regeneration steps; verify the generated example data against the user-provided information.
109109
- Present in `docs/paper/reductions.typ` in tutorial style with clear intuition (see KColoring->QUBO section for reference).
110110

111111
### 6. Create PR (or Resume Existing)
@@ -180,7 +180,7 @@ If execution fails, leave the PR open with the plan commit only — the user can
180180

181181
Structural and quality review is handled by the `review-pipeline` stage, not here. The run stage just needs to produce working code.
182182

183-
**Commit all changes** (implementation):
183+
Ensure all implementation changes are committed before cleanup. A small coherent commit stack is acceptable, especially when resuming an existing PR or integrating subagent work; do not rewrite history just to collapse commits. If there are still uncommitted implementation changes, commit them now:
184184
```bash
185185
git add -A
186186
git commit -m "Implement #<number>: <title>"

.claude/skills/run-pipeline/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ This handles the full pipeline: fetch issue, verify Good label, research, write
167167

168168
### 4. Move to "Review pool"
169169

170-
After `issue-to-pr` succeeds, move the issue to the `Review pool` column:
170+
After `issue-to-pr` fully succeeds, move the issue to the `Review pool` column. "Fully succeeds" means the implementation work is committed, the temporary plan file has been deleted, the PR implementation summary comment has been posted, the branch has been pushed, and the working tree is clean aside from ignored/generated files:
171171

172172
```bash
173173
python3 scripts/pipeline_board.py move <ITEM_ID> review-pool

scripts/pipeline_pr.py

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,73 @@ def is_bot_login(login: str) -> bool:
4949

5050

5151
def extract_linked_issue_number(title: str | None, body: str | None) -> int | None:
52+
issue_numbers = extract_linked_issue_numbers(title, body)
53+
return issue_numbers[0] if issue_numbers else None
54+
55+
56+
def extract_linked_issue_numbers(title: str | None, body: str | None) -> list[int]:
57+
issue_numbers: list[int] = []
58+
59+
def append_unique(number: int) -> None:
60+
if number not in issue_numbers:
61+
issue_numbers.append(number)
62+
5263
for text in [body or "", title or ""]:
53-
match = _CLOSING_ISSUE_RE.search(text)
54-
if match:
55-
return int(match.group(1))
64+
for match in _CLOSING_ISSUE_RE.finditer(text):
65+
append_unique(int(match.group(1)))
5666

5767
for text in [body or "", title or ""]:
58-
match = _GENERIC_ISSUE_RE.search(text)
59-
if match:
60-
return int(match.group(1))
68+
for match in _GENERIC_ISSUE_RE.finditer(text):
69+
append_unique(int(match.group(1)))
70+
71+
return issue_numbers
72+
73+
74+
def extract_linked_issue_number_from_pr_data(pr_data: dict | None) -> int | None:
75+
issue_numbers = extract_linked_issue_numbers_from_pr_data(pr_data)
76+
return max(issue_numbers) if issue_numbers else None
77+
78+
79+
def extract_linked_issue_numbers_from_pr_data(pr_data: dict | None) -> list[int]:
80+
issue_numbers: list[int] = []
81+
82+
def append_unique(number: int) -> None:
83+
if number not in issue_numbers:
84+
issue_numbers.append(number)
85+
86+
if pr_data:
87+
for issue in pr_data.get("closingIssuesReferences") or []:
88+
number = issue.get("number")
89+
if number is not None:
90+
append_unique(int(number))
91+
92+
if issue_numbers:
93+
return issue_numbers
94+
95+
if not pr_data:
96+
return []
97+
98+
return extract_linked_issue_numbers(pr_data.get("title"), pr_data.get("body"))
99+
100+
101+
def _normalized_match_text(text: str | None) -> str:
102+
return " ".join((text or "").lower().split())
61103

62-
return None
104+
105+
def score_linked_issue_candidate(pr_data: dict, issue: dict) -> tuple[int, int]:
106+
score = 0
107+
pr_title = _normalized_match_text(pr_data.get("title"))
108+
pr_body = _normalized_match_text(pr_data.get("body"))
109+
issue_title = _normalized_match_text(issue.get("title"))
110+
issue_number = int(issue.get("number") or -1)
111+
112+
if issue_title:
113+
if issue_title in pr_title:
114+
score += 100
115+
if issue_title in pr_body:
116+
score += 40
117+
118+
return score, issue_number
63119

64120

65121
def normalize_issue_thread_comment(comment: dict) -> dict:
@@ -292,10 +348,7 @@ def build_snapshot(
292348
codecov_summary: dict | None = None,
293349
) -> dict:
294350
if linked_issue_number is None:
295-
linked_issue_number = extract_linked_issue_number(
296-
pr_data.get("title"),
297-
pr_data.get("body"),
298-
)
351+
linked_issue_number = extract_linked_issue_number_from_pr_data(pr_data)
299352

300353
labels = [label.get("name") for label in pr_data.get("labels", []) if label.get("name")]
301354
files = [
@@ -428,7 +481,11 @@ def build_context_result(
428481

429482
def build_pr_context(repo: str, pr_number: int) -> dict:
430483
snapshot = build_pr_snapshot(repo, pr_number)
431-
comments = build_comments_summary(repo, pr_number)
484+
comments = build_comments_summary(
485+
repo,
486+
pr_number,
487+
linked_issue_number=snapshot.get("linked_issue_number"),
488+
)
432489
linked_issue_result = build_linked_issue_context(
433490
repo,
434491
pr_number,
@@ -478,7 +535,8 @@ def fetch_pr_data(repo: str, pr_number: int) -> dict:
478535
"--json",
479536
(
480537
"number,title,body,labels,files,additions,deletions,commits,"
481-
"headRefName,baseRefName,headRefOid,url,state,mergeable,author"
538+
"headRefName,baseRefName,headRefOid,url,state,mergeable,author,"
539+
"closingIssuesReferences"
482540
),
483541
)
484542

@@ -562,10 +620,12 @@ def fetch_check_runs(repo: str, head_sha: str) -> dict:
562620

563621

564622
def fetch_linked_issue_bundle(repo: str, pr_data: dict) -> tuple[int | None, dict | None]:
565-
issue_number = extract_linked_issue_number(pr_data.get("title"), pr_data.get("body"))
566-
if issue_number is None:
623+
issue_numbers = extract_linked_issue_numbers_from_pr_data(pr_data)
624+
if not issue_numbers:
567625
return None, None
568-
return issue_number, fetch_issue_data(repo, issue_number)
626+
issues = [fetch_issue_data(repo, issue_number) for issue_number in issue_numbers]
627+
best_issue = max(issues, key=lambda issue: score_linked_issue_candidate(pr_data, issue))
628+
return int(best_issue["number"]), best_issue
569629

570630

571631
def fetch_ci_summary(repo: str, pr_number: int, pr_data: dict | None = None) -> dict:
@@ -580,12 +640,16 @@ def fetch_ci_summary(repo: str, pr_number: int, pr_data: dict | None = None) ->
580640
return summary
581641

582642

583-
def build_comments_summary(repo: str, pr_number: int, pr_data: dict | None = None) -> dict:
643+
def build_comments_summary(
644+
repo: str,
645+
pr_number: int,
646+
pr_data: dict | None = None,
647+
*,
648+
linked_issue_number: int | None = None,
649+
) -> dict:
584650
pr_data = pr_data or fetch_pr_data(repo, pr_number)
585-
linked_issue_number = extract_linked_issue_number(
586-
pr_data.get("title"),
587-
pr_data.get("body"),
588-
)
651+
if linked_issue_number is None:
652+
linked_issue_number = extract_linked_issue_number_from_pr_data(pr_data)
589653

590654
summary = summarize_comments(
591655
inline_comments=fetch_inline_comments(repo, pr_number),

scripts/test_pipeline_pr.py

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
edit_pr_body,
1616
extract_codecov_summary,
1717
extract_linked_issue_number,
18+
fetch_linked_issue_bundle,
1819
format_issue_context,
1920
post_pr_comment,
2021
parse_args,
@@ -88,6 +89,87 @@ def test_extract_linked_issue_number_falls_back_to_title(self) -> None:
8889
)
8990
self.assertEqual(linked_issue, 117)
9091

92+
@mock.patch("pipeline_pr.fetch_issue_data")
93+
def test_fetch_linked_issue_bundle_prefers_closing_issue_references(
94+
self,
95+
fetch_issue_data: mock.Mock,
96+
) -> None:
97+
fetch_issue_data.return_value = {
98+
"number": 210,
99+
"title": "[Model] Partition",
100+
}
101+
102+
issue_number, issue = fetch_linked_issue_bundle(
103+
"CodingThrust/problem-reductions",
104+
{
105+
"title": "Fix #19: stale title reference",
106+
"body": "This PR still mentions Fixes #19 in copied text.",
107+
"closingIssuesReferences": [
108+
{"number": 210, "title": "[Model] Partition"},
109+
],
110+
},
111+
)
112+
113+
self.assertEqual(issue_number, 210)
114+
self.assertEqual(issue, {"number": 210, "title": "[Model] Partition"})
115+
fetch_issue_data.assert_called_once_with("CodingThrust/problem-reductions", 210)
116+
117+
@mock.patch("pipeline_pr.fetch_issue_data")
118+
def test_fetch_linked_issue_bundle_checks_all_candidates_and_prefers_title_match(
119+
self,
120+
fetch_issue_data: mock.Mock,
121+
) -> None:
122+
issues = {
123+
19: {"number": 19, "title": "[Rule] Coloring -> ILP"},
124+
210: {"number": 210, "title": "[Model] Partition"},
125+
}
126+
fetch_issue_data.side_effect = lambda repo, number: issues[number]
127+
128+
issue_number, issue = fetch_linked_issue_bundle(
129+
"CodingThrust/problem-reductions",
130+
{
131+
"title": "Fix #19: [Model] Partition",
132+
"body": "Also closes #210.",
133+
"closingIssuesReferences": [
134+
{"number": 19},
135+
{"number": 210},
136+
],
137+
},
138+
)
139+
140+
self.assertEqual(issue_number, 210)
141+
self.assertEqual(issue, {"number": 210, "title": "[Model] Partition"})
142+
self.assertEqual(fetch_issue_data.call_args_list, [
143+
mock.call("CodingThrust/problem-reductions", 19),
144+
mock.call("CodingThrust/problem-reductions", 210),
145+
])
146+
147+
@mock.patch("pipeline_pr.fetch_issue_data")
148+
def test_fetch_linked_issue_bundle_uses_latest_issue_when_scores_tie(
149+
self,
150+
fetch_issue_data: mock.Mock,
151+
) -> None:
152+
issues = {
153+
19: {"number": 19, "title": "[Model] Old"},
154+
210: {"number": 210, "title": "[Model] Newer"},
155+
}
156+
fetch_issue_data.side_effect = lambda repo, number: issues[number]
157+
158+
issue_number, issue = fetch_linked_issue_bundle(
159+
"CodingThrust/problem-reductions",
160+
{
161+
"title": "Refresh implementation",
162+
"body": "Touches multiple linked issues.",
163+
"closingIssuesReferences": [
164+
{"number": 19},
165+
{"number": 210},
166+
],
167+
},
168+
)
169+
170+
self.assertEqual(issue_number, 210)
171+
self.assertEqual(issue, {"number": 210, "title": "[Model] Newer"})
172+
91173
def test_summarize_comments_splits_human_copilot_and_codecov_sources(self) -> None:
92174
summary = summarize_comments(
93175
inline_comments=[
@@ -205,6 +287,48 @@ def test_build_snapshot_includes_linked_issue_ci_and_codecov(self) -> None:
205287
self.assertEqual(snapshot["counts"]["files"], 1)
206288
self.assertEqual(snapshot["counts"]["commits"], 2)
207289

290+
def test_build_snapshot_prefers_closing_issue_references_when_inferring_link(self) -> None:
291+
snapshot = build_snapshot(
292+
{
293+
"number": 664,
294+
"title": "Fix #19: stale title reference",
295+
"body": "Copied text still says Fixes #19.",
296+
"state": "OPEN",
297+
"url": "https://github.com/CodingThrust/problem-reductions/pull/664",
298+
"headRefName": "issue-210-partition-v2",
299+
"baseRefName": "main",
300+
"mergeable": "CONFLICTING",
301+
"headRefOid": "abc123",
302+
"labels": [],
303+
"files": [],
304+
"commits": [],
305+
"closingIssuesReferences": [{"number": 210}],
306+
}
307+
)
308+
309+
self.assertEqual(snapshot["linked_issue_number"], 210)
310+
311+
def test_build_snapshot_uses_latest_linked_issue_number_when_multiple_candidates_exist(self) -> None:
312+
snapshot = build_snapshot(
313+
{
314+
"number": 700,
315+
"title": "Refresh implementation",
316+
"body": "References multiple issues.",
317+
"state": "OPEN",
318+
"url": "https://github.com/CodingThrust/problem-reductions/pull/700",
319+
"headRefName": "refresh-linked-issues",
320+
"baseRefName": "main",
321+
"mergeable": "MERGEABLE",
322+
"headRefOid": "def456",
323+
"labels": [],
324+
"files": [],
325+
"commits": [],
326+
"closingIssuesReferences": [{"number": 19}, {"number": 210}],
327+
}
328+
)
329+
330+
self.assertEqual(snapshot["linked_issue_number"], 210)
331+
208332
def test_build_current_pr_context_includes_repo_and_pr_fields(self) -> None:
209333
current = build_current_pr_context(
210334
"CodingThrust/problem-reductions",
@@ -406,7 +530,11 @@ def test_build_pr_context_assembles_existing_helper_results(
406530
context = build_pr_context("CodingThrust/problem-reductions", 570)
407531

408532
build_pr_snapshot.assert_called_once_with("CodingThrust/problem-reductions", 570)
409-
build_comments_summary.assert_called_once_with("CodingThrust/problem-reductions", 570)
533+
build_comments_summary.assert_called_once_with(
534+
"CodingThrust/problem-reductions",
535+
570,
536+
linked_issue_number=117,
537+
)
410538
build_linked_issue_context.assert_called_once()
411539
self.assertEqual(context["pr_number"], 570)
412540
self.assertEqual(context["issue_context_text"], "# [Model] GraphPartitioning")

0 commit comments

Comments
 (0)