Repository navigation
feat(bugfix-detect): broaden classification beyond Conventional Commit prefix - #18
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements a heuristic bug-fix classification system for commit subjects, expanding detection to include keywords and issue closure references. Feedback identifies logic errors in the cross-repo issue reference parser, specifically regarding repository name character sets and parsing boundaries. It also suggests improving test coverage for cross-repo references that lack explicit keywords.
| fn followed_by_issue_ref(rest: &str) -> bool { | ||
| // Allow optional `:` and whitespace, then either `#NNN`, `gh-NNN`, or | ||
| // `<owner>/<repo>#NNN`. We only need a quick check, not a full parser. | ||
| let trimmed = rest.trim_start_matches([':', ' ', '\t']); | ||
| let bytes = trimmed.as_bytes(); | ||
|
|
||
| // Skip an optional `<word>/<word>` prefix (cross-repo references). | ||
| let mut idx = 0; | ||
| while idx < bytes.len() | ||
| && (bytes[idx].is_ascii_alphanumeric() | ||
| || bytes[idx] == b'-' | ||
| || bytes[idx] == b'_' | ||
| || bytes[idx] == b'/') | ||
| { | ||
| idx += 1; | ||
| } | ||
| let after_org = if idx > 0 && idx < bytes.len() && bytes[idx - 1] == b'/' { | ||
| &trimmed[idx..] | ||
| } else { | ||
| trimmed | ||
| }; | ||
| let bytes = after_org.as_bytes(); | ||
|
|
||
| if bytes.starts_with(b"#") && bytes.len() > 1 && bytes[1].is_ascii_digit() { | ||
| return true; | ||
| } | ||
| if (bytes.starts_with(b"gh-") || bytes.starts_with(b"GH-")) | ||
| && bytes.len() > 3 | ||
| && bytes[3].is_ascii_digit() | ||
| { | ||
| return true; | ||
| } | ||
| false | ||
| } |
There was a problem hiding this comment.
The logic for detecting cross-repo issue references (e.g., owner/repo#123) is currently broken.
- The loop at line 116 correctly consumes the repository prefix, but the condition at line 123 bytes[idx - 1] == b'/' is incorrect for standard owner/repo#123 syntax (where the character before # is part of the repo name, not a slash).
- Repository names can also contain dots (.), which are currently not included in the allowed character set in the loop.
- The GH- check is redundant because the input string has already been converted to lowercase.
I suggest updating the logic to check if the loop stopped at a # character and adding . to the allowed set.
fn followed_by_issue_ref(rest: &str) -> bool {
// Allow optional : and whitespace, then either #NNN, gh-NNN, or
// <owner>/<repo>#NNN. We only need a quick check, not a full parser.
let trimmed = rest.trim_start_matches([':', ' ', '\t']);
let bytes = trimmed.as_bytes();
// Skip an optional <owner>/<repo> prefix (cross-repo references).
let mut idx = 0;
while idx < bytes.len()
&& (bytes[idx].is_ascii_alphanumeric()
|| bytes[idx] == b'-'
|| bytes[idx] == b'_'
|| bytes[idx] == b'/'
|| bytes[idx] == b'.')
{
idx += 1;
}
// If we stopped at a '#', we likely skipped a repository prefix.
let after_org = if idx < bytes.len() && bytes[idx] == b'#' {
&trimmed[idx..]
} else {
trimmed
};
let bytes = after_org.as_bytes();
if bytes.starts_with(b"#") && bytes.len() > 1 && bytes[1].is_ascii_digit() {
return true;
}
if bytes.starts_with(b"gh-")
&& bytes.len() > 3
&& bytes[3].is_ascii_digit()
{
return true;
}
false
}There was a problem hiding this comment.
Fixed in c3d902c. Rewrote followed_by_issue_ref(): find # directly, then validate the prefix splits on / into a non-empty owner and repo with only legal repo-name characters (alphanumeric, dots, dashes, underscores). Also dropped the redundant uppercase GH- check (input is already lowercased) and added cross-repo cases to the test suite (agent-sh/agnix#900, repos with dots and underscores, malformed refs).
| let delta = make_delta(vec![ | ||
| make_commit("alice", "fix: explicit conventional fix", file()), | ||
| make_commit("alice", "Fix race condition in worker pool", file()), | ||
| make_commit("alice", "Resolves #42", file()), | ||
| make_commit("alice", "hotfix for prod outage", file()), | ||
| make_commit("alice", "feat: add login flow", file()), | ||
| make_commit("alice", "Add prefix support to parser", file()), | ||
| ]); | ||
|
|
||
| merge_delta(&mut map, &delta); | ||
|
|
||
| let activity = &map.file_activity["src/lib.rs"]; | ||
| assert_eq!( | ||
| activity.bug_fix_changes, 4, | ||
| "expected 4 bug-fix commits (conventional + race + resolves + hotfix)" | ||
| ); |
There was a problem hiding this comment.
The current test suite for bug-fix detection does not verify cross-repo issue references that lack a separate keyword (like Fix). Adding such a case would ensure the has_issue_closure logic is working correctly independently of the keyword heuristic.
let delta = make_delta(vec![
make_commit("alice", "fix: explicit conventional fix", file()),
make_commit("alice", "Fix race condition in worker pool", file()),
make_commit("alice", "Resolves #42", file()),
make_commit("alice", "Resolves agent-sh/agnix#900", file()),
make_commit("alice", "hotfix for prod outage", file()),
make_commit("alice", "feat: add login flow", file()),
make_commit("alice", "Add prefix support to parser", file()),
]);
merge_delta(&mut map, &delta);
let activity = &map.file_activity["src/lib.rs"];
assert_eq!(
activity.bug_fix_changes, 5,
"expected 5 bug-fix commits (conventional + race + resolves + cross-repo + hotfix)"
);There was a problem hiding this comment.
Done in c3d902c — added Closes agent-sh/agnix#900 (no fix keyword) to the aggregator integration test, bumping the expected count to 5 and exercising the cross-repo branch independently of the keyword heuristic.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7056eee. Configure here.
| &trimmed[idx..] | ||
| } else { | ||
| trimmed | ||
| }; |
There was a problem hiding this comment.
Cross-repo issue reference parsing never matches
Medium Severity
followed_by_issue_ref fails to detect cross-repo references like owner/repo#NNN. The while loop consumes all alphanumeric, -, _, and / characters, so for "agent-sh/agnix#900" it eats agent-sh/agnix (stopping at # with idx=14). Then bytes[idx - 1] == b'/' checks the last consumed char, which is x (not /), so after_org falls back to the full string — which doesn't start with #. A subject like "Closes org/repo#42" (no fix keyword) silently returns false. The test on line 221 is masked because "Fix" triggers the keyword path, not the closure path.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7056eee. Configure here.
There was a problem hiding this comment.
Fixed in c3d902c — replaced the broken character-eating loop with: find # directly, then validate prefix contains / and both segments use only repo-name-legal characters. Closes agent-sh/agnix#900 and Closes org/repo#42 (no fix keyword) now register correctly.
…t prefix
Background: bug_fix_changes was previously incremented only when a commit
subject started with `fix:`. That misses every project that does not
enforce Conventional Commits, plus several repos that *do* use it but
file regressions under different verbs ("Fix race", "Hotfix for prod
outage", "Resolves #42", "revert sketchy commit", etc.). On agnix this
under-counted bug-fix activity even though the commit history is full
of plain English fix subjects.
This adds a new analyzer-core::bug_fix_detect module with a single
public function, is_bug_fix(subject), and wires it into the aggregator
in place of the prefix-only check.
Heuristic returns true when any of:
1. Conventional Commit prefix is one of: fix, bugfix, hotfix, patch,
revert (so "patch:", "revert:", and "hotfix:" now also count).
2. Subject contains a fix-related whole-word keyword:
fix/fixed/fixes/fixing, bug/bugfix/hotfix, patch/patched,
revert/reverts/reverted, regression, race, deadlock, leak, crash,
oops, typo, mistake, broken.
3. Subject contains an issue-closure phrase: "fixes #123",
"closes #42", "resolves GH-7", "resolves owner/repo#900".
False-positive guard: matching is whole-word (case-insensitive) so
"prefix", "suffix", "affix", "postfix", "unfixable" do not register.
"Close" without an issue ref also does not register, so
"Close the modal on ESC" stays out.
16 new unit tests in bug_fix_detect.rs cover every branch (positives,
negatives, mixed case, issue-closure variants, the false-positive set).
One aggregator-level test confirms the wired path counts plain-English
fix subjects against bug_fix_changes.
What this affects downstream:
- bugspots query density should rise for repos that don't use `fix:`
- file_activity.bug_fix_changes and last_bug_fix become more accurate
- diff-risk's bug_fix_rate term gets a more honest denominator
Tests: 178 passing across the workspace (was 162), clippy clean.
Reviewer (gemini, cursor) caught that followed_by_issue_ref() never hit the <owner>/<repo>#NNN form. The character-eating loop consumed "agent-sh/agnix" but then checked bytes[idx-1] against b'/' - which matched the last char of "agnix", not the slash inside, so the slice-back logic always fell through to "doesn't start with #". Rewritten to find '#' directly, then validate that what came before contains a slash and otherwise looks like owner/repo (alphanumeric + . _ -). This also fixes: - repo names with dots (e.g. "my.repo.name") were rejected - redundant uppercase "GH-" check (input is already lowercased) - "Closes /#1" no longer matches (owner segment must be non-empty) Tests: add cross_repo_issue_refs_hit (4 cases including agent-sh/agnix#900, repos with dots/underscores), add malformed_cross_repo_refs_do_not_hit (3 cases), and add a cross-repo case to the aggregator integration test. Reviewer: gemini-code-assist#18 (high), cursor#18 (medium)
c3d902c to
25f89c3
Compare


Why
bug_fix_changeswas previously incremented only when a commit subject started withfix:. That misses every project that does not enforce Conventional Commits, plus several repos that do use it but file regressions under different verbs - "Fix race", "Hotfix for prod outage", "Resolves #42", "revert sketchy commit", etc. On agnix this under-counted bug-fix activity even though the commit history is full of plain English fix subjects.What changes
New
analyzer-core::bug_fix_detectmodule with one public function,is_bug_fix(subject), wired into the aggregator in place of the prefix-only check.Heuristic returns
truewhen any of:fix,bugfix,hotfix,patch,revert(sopatch:,revert:, andhotfix:now also count).fix,fixed,fixes,fixing,bug,bugfix,hotfix,patch,patched,revert,reverts,reverted,regression,race,deadlock,leak,crash,crashed,oops,typo,mistake,broken.fixes #123,closes #42,resolves GH-7,resolves owner/repo#900.False-positive guard
Matching is whole-word (case-insensitive), so
prefix,suffix,affix,postfix,unfixabledo not register.closewithout an issue ref also does not register, soClose the modal on ESCstays out.Tests
bug_fix_detect.rscover every branch (positives, negatives, mixed case, issue-closure variants, the false-positive set).bug_fix_changes.What this affects downstream
bugspotsquery density rises for repos that don't usefix:file_activity.bug_fix_changesandlast_bug_fixbecome more accuratebug_fix_rateterm gets a more honest denominatorTest plan
cargo test --workspace- 178 tests pass (was 162)cargo clippy --workspace --all-targets -- -D warnings- cleancargo fmt --all- cleanIndependence
Stacks cleanly with #17 (drop-AI-detection) - the two PRs touch different code paths and either order works. They can land in any order.
Note
Medium Risk
Changes the definition of what counts as a bug-fix commit, which will alter downstream metrics (
bug_fix_changes,last_bug_fix, and any derived scores) and could introduce false positives/negatives despite added tests.Overview
Improves bug-fix attribution by replacing the aggregator’s strict
prefix == "fix"check with a new heuristicis_bug_fix()matcher.The new detection counts fixes not only via Conventional Commit prefixes (e.g.
fix,hotfix,revert,patch) but also via whole-word fix keywords and issue-closure phrases (e.g.closes #42,resolves GH-7), with tests added for boundary/false-positive cases and an aggregator-level test to ensure freeform subjects incrementbug_fix_changes.Reviewed by Cursor Bugbot for commit 7056eee. Configure here.