fix: handle multi-hyphen prefixes with hash IDs (#419)

Fixed bug where issue IDs with multiple hyphens in the prefix and hash
suffixes were incorrectly parsed. For example, `web-app-a3f8e9` was
being parsed with prefix `web-` instead of `web-app`.

Root cause: ExtractIssuePrefix() only checked for numeric suffixes.
When it encountered a hash suffix, it fell back to using the first
hyphen instead of the last hyphen.

Changes:
- Added isLikelyHash() helper to detect hexadecimal hash suffixes (4-8 chars)
- Updated ExtractIssuePrefix() to handle both numeric and hash suffixes
- Added comprehensive test cases for various prefix patterns

Test coverage includes:
- web-app-123 (numeric suffix)
- web-app-a3f8e9 (hash suffix)
- my-cool-app-a3f8e9 (three-part prefix with hash)
- super-long-project-name-1a2b (four-part prefix)
- Various hash lengths and case variations

Co-authored-by: dubstylee <dubstylee@users.noreply.github.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-29 15:05:10 -08:00
parent f321857f18
commit 8becf0d9b9
3 changed files with 69 additions and 5 deletions

View File

@@ -367,6 +367,41 @@ func TestExtractIssuePrefix(t *testing.T) {
issueID: "beads-vscode-1",
expected: "beads-vscode", // Last hyphen before numeric suffix
},
{
name: "web-app style prefix",
issueID: "web-app-123",
expected: "web-app", // Should extract "web-app", not "web-"
},
{
name: "three-part prefix with hash",
issueID: "my-cool-app-a3f8e9",
expected: "my-cool-app", // Hash suffix should use last hyphen logic
},
{
name: "four-part prefix with 4-char hash",
issueID: "super-long-project-name-1a2b",
expected: "super-long-project-name", // 4-char hash
},
{
name: "prefix with 5-char hash",
issueID: "my-app-1a2b3",
expected: "my-app", // 5-char hash
},
{
name: "prefix with 6-char hash",
issueID: "web-app-a1b2c3",
expected: "web-app", // 6-char hash
},
{
name: "uppercase hash",
issueID: "my-app-A3F8E9",
expected: "my-app", // Uppercase hash should work
},
{
name: "mixed case hash",
issueID: "proj-AbCd12",
expected: "proj", // Mixed case hash should work
},
}
for _, tt := range tests {

View File

@@ -6,8 +6,11 @@ import (
)
// ExtractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
// Uses the last hyphen before a numeric suffix, so "beads-vscode-1" -> "beads-vscode"
// For non-numeric suffixes like "vc-baseline-test", returns the first segment "vc"
// Uses the last hyphen before a numeric or hash suffix:
// - "beads-vscode-1" -> "beads-vscode" (numeric suffix)
// - "web-app-a3f8e9" -> "web-app" (hash suffix)
// - "my-cool-app-123" -> "my-cool-app" (numeric suffix)
// Only uses first hyphen for non-ID suffixes like "vc-baseline-test" -> "vc"
func ExtractIssuePrefix(issueID string) string {
// Try last hyphen first (handles multi-part prefixes like "beads-vscode-1")
lastIdx := strings.LastIndex(issueID, "-")
@@ -16,21 +19,30 @@ func ExtractIssuePrefix(issueID string) string {
}
suffix := issueID[lastIdx+1:]
// Check if suffix is numeric (or starts with a number for hierarchical IDs like "bd-123.1")
// Check if suffix looks like an issue ID component (numeric or hash-like)
if len(suffix) > 0 {
// Extract just the numeric part (handle "123.1.2" -> check "123")
numPart := suffix
if dotIdx := strings.Index(suffix, "."); dotIdx > 0 {
numPart = suffix[:dotIdx]
}
// Check if it's numeric
var num int
if _, err := fmt.Sscanf(numPart, "%d", &num); err == nil {
// Suffix is numeric, use last hyphen
return issueID[:lastIdx]
}
// Check if it looks like a hash (hexadecimal characters, 4+ chars)
// Hash IDs are typically 4-8 hex characters (e.g., "a3f8e9", "1a2b")
if isLikelyHash(numPart) {
// Suffix looks like a hash, use last hyphen
return issueID[:lastIdx]
}
}
// Suffix is not numeric (e.g., "vc-baseline-test"), fall back to first hyphen
// Suffix is not numeric or hash-like (e.g., "vc-baseline-test"), fall back to first hyphen
firstIdx := strings.Index(issueID, "-")
if firstIdx <= 0 {
return ""
@@ -38,6 +50,22 @@ func ExtractIssuePrefix(issueID string) string {
return issueID[:firstIdx]
}
// isLikelyHash checks if a string looks like a hash ID suffix.
// Returns true for hexadecimal strings of 4-8 characters.
// Hash IDs in beads are typically 4-6 characters (progressive length scaling).
func isLikelyHash(s string) bool {
if len(s) < 4 || len(s) > 8 {
return false
}
// Check if all characters are hexadecimal
for _, c := range s {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return false
}
}
return true
}
// ExtractIssueNumber extracts the number from an issue ID like "bd-123" -> 123
func ExtractIssueNumber(issueID string) int {
idx := strings.LastIndex(issueID, "-")