From 8becf0d9b991ca91e58dd2484e1f8b923466e828 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 29 Nov 2025 15:05:10 -0800 Subject: [PATCH] fix: handle multi-hyphen prefixes with hash IDs (#419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .beads/issues.jsonl | 3 ++- internal/utils/id_parser_test.go | 35 +++++++++++++++++++++++++++++++ internal/utils/issue_id.go | 36 ++++++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ce53b71c..ccae87ee 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1 +1,2 @@ -{"id":"bd-bx9","title":"bd init --contributor should configure sync.remote=upstream for fork workflows","description":"When running `bd init --contributor` in a fork workflow (where `upstream` remote points to the original repo), the wizard should configure beads to sync from `upstream/main` rather than `origin/main`.\n\n**Current behavior:**\n- Contributor mode detects the fork setup (upstream remote exists)\n- Sets up planning repo and auto-routing\n- Does NOT configure sync remote\n- `bd sync` on feature branches shows \"No upstream configured, using --from-main mode\" and syncs from `origin/main`\n\n**Expected behavior:**\n- Contributor mode should also set `sync.remote = upstream` (or similar config)\n- `bd sync` should pull beads from `upstream/main` (source of truth)\n\n**Why this matters:**\n- The fork's `origin/main` may be behind `upstream/main`\n- Contributors want the latest issues from the source repo\n- Code PRs go: local -\u003e origin -\u003e upstream, but beads should come FROM upstream\n\n**Suggested fix:**\nAdd to `runContributorWizard()` after detecting fork:\n```go\nif isFork {\n store.SetConfig(ctx, \"sync.remote\", \"upstream\")\n}\n```","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-29T00:39:05.137488727-05:00","updated_at":"2025-11-29T00:39:05.137488727-05:00","labels":["contributor","sync"]} +{"id":"bd-8da","title":"Fix prefix extraction for multi-hyphen prefixes with hash IDs","description":"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 as prefix 'web-' instead of 'web-app'.\n\nThe root cause was in ExtractIssuePrefix() which only checked for numeric suffixes. When it encountered a hash suffix (like 'a3f8e9'), it would fall back to using the first hyphen instead of the last hyphen.\n\nThe fix adds hash detection logic via isLikelyHash() which checks if a suffix looks like a hexadecimal hash (4-8 characters, all hex digits). This allows the function to correctly handle both numeric and hash suffixes.\n\nTest cases added for:\n- web-app-123 (numeric suffix)\n- web-app-a3f8e9 (hash suffix) \n- my-cool-app-a3f8e9 (three-part prefix with hash)\n- super-long-project-name-1a2b (four-part prefix with hash)\n- Various hash lengths (4-6 chars)\n- Uppercase and mixed-case hashes\n\nFiles modified:\n- internal/utils/issue_id.go: Added isLikelyHash() and updated ExtractIssuePrefix()\n- internal/utils/id_parser_test.go: Added comprehensive test cases\n\nThis fix enables beads to work correctly with projects that have hyphens in their names.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-11-29T15:01:09.544178-05:00","updated_at":"2025-11-29T15:01:18.385453-05:00","closed_at":"2025-11-29T15:01:18.385453-05:00"} +{"id":"bd-bx9","title":"bd init --contributor should configure sync.remote=upstream for fork workflows","description":"When running `bd init --contributor` in a fork workflow (where `upstream` remote points to the original repo), the wizard should configure beads to sync from `upstream/main` rather than `origin/main`.\n\n**Current behavior:**\n- Contributor mode detects the fork setup (upstream remote exists)\n- Sets up planning repo and auto-routing\n- Does NOT configure sync remote\n- `bd sync` on feature branches shows \"No upstream configured, using --from-main mode\" and syncs from `origin/main`\n\n**Expected behavior:**\n- Contributor mode should also set `sync.remote = upstream` (or similar config)\n- `bd sync` should pull beads from `upstream/main` (source of truth)\n\n**Why this matters:**\n- The fork's `origin/main` may be behind `upstream/main`\n- Contributors want the latest issues from the source repo\n- Code PRs go: local -\u003e origin -\u003e upstream, but beads should come FROM upstream\n\n**Suggested fix:**\nAdd to `runContributorWizard()` after detecting fork:\n```go\nif isFork {\n store.SetConfig(ctx, \"sync.remote\", \"upstream\")\n}\n```","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-29T00:39:05.137488727-05:00","updated_at":"2025-11-29T00:39:05.137488727-05:00"} diff --git a/internal/utils/id_parser_test.go b/internal/utils/id_parser_test.go index 266ba307..be926294 100644 --- a/internal/utils/id_parser_test.go +++ b/internal/utils/id_parser_test.go @@ -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 { diff --git a/internal/utils/issue_id.go b/internal/utils/issue_id.go index f3bb8591..a7979741 100644 --- a/internal/utils/issue_id.go +++ b/internal/utils/issue_id.go @@ -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, "-")