fix(utils): parse multi-hyphen prefixes correctly (GH#405)

The ExtractIssuePrefix function was falling back to first-hyphen
extraction when the suffix didn't look like a hash (e.g., 4+ char
words without digits). This broke prefixes like 'hacker-news' where
an issue ID 'hacker-news-test' would incorrectly extract 'hacker'.

Fix: Always use last-hyphen extraction for alphanumeric suffixes.
Only fall back to first-hyphen for non-alphanumeric suffixes.

Examples:
- 'hacker-news-test' -> 'hacker-news' (was: 'hacker')
- 'me-py-toolkit-abc' -> 'me-py-toolkit' (was: 'me')
- 'vc-baseline-hello' -> 'vc-baseline' (was: 'vc')

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-16 01:13:15 -08:00
parent 50608ce3a5
commit 77aeb50d27
5 changed files with 252 additions and 235 deletions

2
.beads/.gitignore vendored
View File

@@ -30,3 +30,5 @@ beads.right.meta.json
!issues.jsonl !issues.jsonl
!metadata.json !metadata.json
!config.json !config.json
deletions.jsonl
deletions.jsonl.migrated

File diff suppressed because one or more lines are too long

View File

@@ -360,7 +360,7 @@ func TestExtractIssuePrefix(t *testing.T) {
{ {
name: "multi-part non-numeric suffix", name: "multi-part non-numeric suffix",
issueID: "vc-baseline-test", issueID: "vc-baseline-test",
expected: "vc", // Falls back to first hyphen for non-numeric suffix expected: "vc-baseline", // Alphanumeric suffix uses last hyphen (GH#405 fix)
}, },
{ {
name: "beads-vscode style prefix", name: "beads-vscode style prefix",

View File

@@ -43,16 +43,18 @@ func TestExtractIssuePrefixAllLetterHash(t *testing.T) {
} }
} }
// TestExtractIssuePrefixWordSuffix ensures 4+ char word suffixes still work // TestExtractIssuePrefixWordSuffix tests alphanumeric suffixes (GH#405 fix)
// With the GH#405 fix, all alphanumeric suffixes use last-hyphen extraction,
// even if they look like English words. This fixes multi-hyphen prefix parsing.
func TestExtractIssuePrefixWordSuffix(t *testing.T) { func TestExtractIssuePrefixWordSuffix(t *testing.T) {
// These should use first-hyphen extraction (word suffixes, not hashes) // These should use last-hyphen extraction (alphanumeric = valid issue ID suffix)
wordSuffixes := []struct { wordSuffixes := []struct {
issueID string issueID string
expected string expected string
}{ }{
{"vc-baseline-test", "vc"}, // 4-char "test" - word, not hash {"vc-baseline-test", "vc-baseline"}, // GH#405: alphanumeric suffix uses last hyphen
{"vc-baseline-hello", "vc"}, // 5-char word {"vc-baseline-hello", "vc-baseline"}, // GH#405: alphanumeric suffix uses last hyphen
{"vc-some-feature", "vc"}, // multi-word suffix {"vc-some-feature", "vc-some"}, // GH#405: alphanumeric suffix uses last hyphen
} }
for _, tc := range wordSuffixes { for _, tc := range wordSuffixes {

View File

@@ -6,11 +6,14 @@ import (
) )
// ExtractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd" // ExtractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
// Uses the last hyphen before a numeric or hash suffix: // Uses the last hyphen before an alphanumeric suffix:
// - "beads-vscode-1" -> "beads-vscode" (numeric suffix) // - "beads-vscode-1" -> "beads-vscode" (numeric suffix)
// - "web-app-a3f8e9" -> "web-app" (hash suffix) // - "web-app-a3f8e9" -> "web-app" (hash suffix)
// - "my-cool-app-123" -> "my-cool-app" (numeric suffix) // - "my-cool-app-123" -> "my-cool-app" (numeric suffix)
// Only uses first hyphen for non-ID suffixes like "vc-baseline-test" -> "vc" // - "hacker-news-test" -> "hacker-news" (alphanumeric suffix, GH#405)
//
// Only uses first hyphen when suffix contains non-alphanumeric characters,
// which indicates it's not an issue ID but something like a project name.
func ExtractIssuePrefix(issueID string) string { func ExtractIssuePrefix(issueID string) string {
// Try last hyphen first (handles multi-part prefixes like "beads-vscode-1") // Try last hyphen first (handles multi-part prefixes like "beads-vscode-1")
lastIdx := strings.LastIndex(issueID, "-") lastIdx := strings.LastIndex(issueID, "-")
@@ -19,30 +22,35 @@ func ExtractIssuePrefix(issueID string) string {
} }
suffix := issueID[lastIdx+1:] suffix := issueID[lastIdx+1:]
// Check if suffix looks like an issue ID component (numeric or hash-like) if len(suffix) == 0 {
if len(suffix) > 0 { // Trailing hyphen like "bd-" - return prefix before the hyphen
// Extract just the numeric part (handle "123.1.2" -> check "123") return issueID[:lastIdx]
numPart := suffix }
if dotIdx := strings.Index(suffix, "."); dotIdx > 0 {
numPart = suffix[:dotIdx]
}
// Check if it's numeric // Extract the base part before any dot (handle "123.1.2" -> check "123")
var num int basePart := suffix
if _, err := fmt.Sscanf(numPart, "%d", &num); err == nil { if dotIdx := strings.Index(suffix, "."); dotIdx > 0 {
// Suffix is numeric, use last hyphen basePart = suffix[:dotIdx]
return issueID[:lastIdx] }
}
// Check if it looks like a hash (hexadecimal characters, 4+ chars) // Check if basePart is alphanumeric (valid issue ID suffix)
// Hash IDs are typically 4-8 hex characters (e.g., "a3f8e9", "1a2b") // Issue IDs are always alphanumeric: numeric (1, 23) or hash (a3f, xyz, test)
if isLikelyHash(numPart) { isAlphanumeric := len(basePart) > 0
// Suffix looks like a hash, use last hyphen for _, c := range basePart {
return issueID[:lastIdx] if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
isAlphanumeric = false
break
} }
} }
// Suffix is not numeric or hash-like (e.g., "vc-baseline-test"), fall back to first hyphen // If suffix is alphanumeric, this is an issue ID - use last hyphen
// This handles all issue ID formats including word-like hashes (GH#405)
if isAlphanumeric {
return issueID[:lastIdx]
}
// Suffix contains special characters - not a standard issue ID
// Fall back to first hyphen for cases like project names with descriptions
firstIdx := strings.Index(issueID, "-") firstIdx := strings.Index(issueID, "-")
if firstIdx <= 0 { if firstIdx <= 0 {
return "" return ""