diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 18fa99f1..f319c852 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -731,6 +731,11 @@ func deriveBeadsPrefix(name string) string { return r == '-' || r == '_' }) + // If single part, try to detect compound words (e.g., "gastown" -> "gas" + "town") + if len(parts) == 1 { + parts = splitCompoundWord(parts[0]) + } + if len(parts) >= 2 { // Take first letter of each part: "gas-town" -> "gt" prefix := "" @@ -749,6 +754,27 @@ func deriveBeadsPrefix(name string) string { return strings.ToLower(name[:2]) } +// splitCompoundWord attempts to split a compound word into its components. +// Common suffixes like "town", "ville", "port" are detected to split +// compound names (e.g., "gastown" -> ["gas", "town"]). +func splitCompoundWord(word string) []string { + word = strings.ToLower(word) + + // Common suffixes for compound place names + suffixes := []string{"town", "ville", "port", "place", "land", "field", "wood", "ford"} + + for _, suffix := range suffixes { + if strings.HasSuffix(word, suffix) && len(word) > len(suffix) { + prefix := word[:len(word)-len(suffix)] + if len(prefix) > 0 { + return []string{prefix, suffix} + } + } + } + + return []string{word} +} + // detectBeadsPrefixFromConfig reads the issue prefix from a beads config.yaml file. // Returns empty string if the file doesn't exist or doesn't contain a prefix. // Falls back to detecting prefix from existing issues in issues.jsonl. diff --git a/internal/rig/manager_test.go b/internal/rig/manager_test.go index fb22e1e8..349355fc 100644 --- a/internal/rig/manager_test.go +++ b/internal/rig/manager_test.go @@ -485,3 +485,94 @@ func TestInitBeadsRejectsInvalidPrefix(t *testing.T) { }) } } + +func TestDeriveBeadsPrefix(t *testing.T) { + tests := []struct { + name string + want string + }{ + // Compound words with common suffixes should split + {"gastown", "gt"}, // gas + town + {"nashville", "nv"}, // nash + ville + {"bridgeport", "bp"}, // bridge + port + {"someplace", "sp"}, // some + place + {"greenland", "gl"}, // green + land + {"springfield", "sf"}, // spring + field + {"hollywood", "hw"}, // holly + wood + {"oxford", "of"}, // ox + ford + + // Hyphenated names + {"my-project", "mp"}, + {"gas-town", "gt"}, + {"some-long-name", "sln"}, + + // Underscored names + {"my_project", "mp"}, + + // Short single words (use the whole name) + {"foo", "foo"}, + {"bar", "bar"}, + {"ab", "ab"}, + + // Longer single words without known suffixes (first 2 chars) + {"myrig", "my"}, + {"awesome", "aw"}, + {"coolrig", "co"}, + + // With language suffixes stripped + {"myproject-py", "my"}, + {"myproject-go", "my"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := deriveBeadsPrefix(tt.name) + if got != tt.want { + t.Errorf("deriveBeadsPrefix(%q) = %q, want %q", tt.name, got, tt.want) + } + }) + } +} + +func TestSplitCompoundWord(t *testing.T) { + tests := []struct { + word string + want []string + }{ + // Known suffixes + {"gastown", []string{"gas", "town"}}, + {"nashville", []string{"nash", "ville"}}, + {"bridgeport", []string{"bridge", "port"}}, + {"someplace", []string{"some", "place"}}, + {"greenland", []string{"green", "land"}}, + {"springfield", []string{"spring", "field"}}, + {"hollywood", []string{"holly", "wood"}}, + {"oxford", []string{"ox", "ford"}}, + + // Just the suffix (should not split) + {"town", []string{"town"}}, + {"ville", []string{"ville"}}, + + // No known suffix + {"myrig", []string{"myrig"}}, + {"awesome", []string{"awesome"}}, + + // Empty prefix would result (should not split) + // Note: "town" itself shouldn't split to ["", "town"] + } + + for _, tt := range tests { + t.Run(tt.word, func(t *testing.T) { + got := splitCompoundWord(tt.word) + if len(got) != len(tt.want) { + t.Errorf("splitCompoundWord(%q) = %v, want %v", tt.word, got, tt.want) + return + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("splitCompoundWord(%q)[%d] = %q, want %q", tt.word, i, got[i], tt.want[i]) + } + } + }) + } +}