fix: improve issue ID prefix extraction for word-like suffixes

Refine ExtractIssuePrefix to better distinguish hash IDs from English
words in multi-part issue IDs. Hash suffixes now require digits or be
exactly 3 chars, preventing "test", "gate", "part" from being treated
as hashes. This fixes prefix extraction for IDs like "vc-baseline-test".

Also updates git hooks to use -q flag and adds AGENTS.md documentation.

🤖 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 13:18:30 -08:00
parent c95bc6c21d
commit aed166fe85
9 changed files with 85 additions and 40 deletions

View File

@@ -358,9 +358,9 @@ func TestExtractIssuePrefix(t *testing.T) {
expected: "alpha-beta", // Last hyphen before numeric suffix
},
{
name: "multi-part non-numeric suffix",
name: "multi-part non-numeric suffix (word-like)",
issueID: "vc-baseline-test",
expected: "vc-baseline", // Alphanumeric suffix uses last hyphen (GH#405 fix)
expected: "vc", // Word-like suffix (4+ chars, no digit) uses first hyphen (bd-fasa fix)
},
{
name: "beads-vscode style prefix",

View File

@@ -43,18 +43,20 @@ func TestExtractIssuePrefixAllLetterHash(t *testing.T) {
}
}
// 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.
// TestExtractIssuePrefixWordSuffix tests word-like suffixes (bd-fasa regression)
// Word-like suffixes (4+ chars, no digits) use first-hyphen extraction because
// they're likely multi-part IDs where everything after first hyphen is the ID.
// This fixes bd-fasa regression where word-like suffixes were wrongly treated as hashes.
func TestExtractIssuePrefixWordSuffix(t *testing.T) {
// These should use last-hyphen extraction (alphanumeric = valid issue ID suffix)
// Word-like suffixes (4+ chars, no digits) should use first-hyphen extraction
// The entire part after first hyphen is the ID, not just the last segment
wordSuffixes := []struct {
issueID string
expected string
}{
{"vc-baseline-test", "vc-baseline"}, // GH#405: alphanumeric suffix uses last hyphen
{"vc-baseline-hello", "vc-baseline"}, // GH#405: alphanumeric suffix uses last hyphen
{"vc-some-feature", "vc-some"}, // GH#405: alphanumeric suffix uses last hyphen
{"vc-baseline-test", "vc"}, // bd-fasa: "baseline-test" is the ID, not "test"
{"vc-baseline-hello", "vc"}, // bd-fasa: "baseline-hello" is the ID
{"vc-some-feature", "vc"}, // bd-fasa: "some-feature" is the ID
}
for _, tc := range wordSuffixes {

View File

@@ -6,14 +6,18 @@ import (
)
// ExtractIssuePrefix extracts the prefix from an issue ID like "bd-123" -> "bd"
// Uses the last hyphen before an alphanumeric suffix:
// Uses the last hyphen before a numeric or hash-like suffix:
// - "beads-vscode-1" -> "beads-vscode" (numeric suffix)
// - "web-app-a3f8e9" -> "web-app" (hash suffix)
// - "web-app-a3f8e9" -> "web-app" (hash suffix with digits)
// - "my-cool-app-123" -> "my-cool-app" (numeric suffix)
// - "hacker-news-test" -> "hacker-news" (alphanumeric suffix, GH#405)
// - "bd-a3f" -> "bd" (3-char hash)
//
// Only uses first hyphen when suffix contains non-alphanumeric characters,
// which indicates it's not an issue ID but something like a project name.
// Falls back to first hyphen when suffix looks like an English word (4+ chars, no digits):
// - "vc-baseline-test" -> "vc" (word-like suffix: "test" is not a hash)
// - "bd-multi-part-id" -> "bd" (word-like suffix: "id" is too short but "part-id" path)
//
// This distinguishes hash IDs (which may contain letters but have digits or are 3 chars)
// from multi-part IDs where the suffix after the first hyphen is the entire ID.
func ExtractIssuePrefix(issueID string) string {
// Try last hyphen first (handles multi-part prefixes like "beads-vscode-1")
lastIdx := strings.LastIndex(issueID, "-")
@@ -33,24 +37,15 @@ func ExtractIssuePrefix(issueID string) string {
basePart = suffix[:dotIdx]
}
// Check if basePart is alphanumeric (valid issue ID suffix)
// Issue IDs are always alphanumeric: numeric (1, 23) or hash (a3f, xyz, test)
isAlphanumeric := len(basePart) > 0
for _, c := range basePart {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
isAlphanumeric = false
break
}
}
// 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 {
// Check if this looks like a valid issue ID suffix (numeric or hash-like)
// Use isLikelyHash which requires digits for 4+ char suffixes to avoid
// treating English words like "test", "gate", "part" as hash IDs
if isNumeric(basePart) || isLikelyHash(basePart) {
return issueID[:lastIdx]
}
// Suffix contains special characters - not a standard issue ID
// Fall back to first hyphen for cases like project names with descriptions
// Suffix looks like an English word (4+ chars, no digits) or contains special chars
// Fall back to first hyphen - the entire part after first hyphen is the ID
firstIdx := strings.Index(issueID, "-")
if firstIdx <= 0 {
return ""
@@ -58,6 +53,19 @@ func ExtractIssuePrefix(issueID string) string {
return issueID[:firstIdx]
}
// isNumeric checks if a string contains only digits
func isNumeric(s string) bool {
if len(s) == 0 {
return false
}
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return true
}
// isLikelyHash checks if a string looks like a hash ID suffix.
// Returns true for base36 strings of 3-8 characters (0-9, a-z).
//