Remove sequential ID generation and SyncAllCounters (bd-c7af, bd-8e05, bd-4c74)

- Removed SyncAllCounters() and all call sites (already no-op with hash IDs)
- Removed AllocateNextID() and getNextIDForPrefix() - sequential ID generation
- Removed collision remapping logic in internal/storage/sqlite/collision.go
- Removed rename collision handling in internal/importer/importer.go
- Removed branch-merge example (collision resolution no longer needed)
- Updated EXTENDING.md to remove counter sync examples

These were all deprecated code paths for sequential IDs that are obsolete
with hash-based IDs. Hash ID collisions are handled by extending the hash,
not by remapping to new sequential IDs.
This commit is contained in:
Steve Yegge
2025-10-30 22:24:42 -07:00
parent 4a21005a31
commit 5d137ffeeb
11 changed files with 40 additions and 448 deletions

View File

@@ -631,10 +631,7 @@ if err := store.CreateIssues(ctx, issues, "import"); err != nil {
log.Fatal(err)
}
// If you used explicit IDs, sync counters to prevent collisions
if err := store.SyncAllCounters(ctx); err != nil {
log.Fatal(err)
}
// REMOVED (bd-c7af): SyncAllCounters - no longer needed with hash IDs
```
### Performance Comparison
@@ -695,10 +692,7 @@ func ImportFromExternal(externalIssues []ExternalIssue) error {
return fmt.Errorf("batch create failed: %w", err)
}
// Sync counters since we used explicit IDs
if err := store.SyncAllCounters(ctx); err != nil {
return fmt.Errorf("counter sync failed: %w", err)
}
// REMOVED (bd-c7af): SyncAllCounters - no longer needed with hash IDs
return nil
}

View File

@@ -711,10 +711,7 @@ func TestImportCounterSyncAfterHighID(t *testing.T) {
t.Fatalf("Failed to import high ID issue: %v", err)
}
// Step 4: Sync counters after import (mimics import command behavior)
if err := testStore.SyncAllCounters(ctx); err != nil {
t.Fatalf("Failed to sync counters: %v", err)
}
// REMOVED (bd-c7af): Counter sync - no longer needed with hash IDs
// Step 5: Create another auto-generated issue
// This should get bd-101 (counter should have synced to 100), not bd-4

View File

@@ -151,11 +151,7 @@ func profileImportOperation(t *testing.T, numIssues int) {
phases["create_update"] = time.Since(createStart)
// Phase 4: Sync counters
syncStart := time.Now()
if err := sqliteStore.SyncAllCounters(ctx); err != nil {
t.Fatalf("Failed to sync counters: %v", err)
}
phases["sync_counters"] = time.Since(syncStart)
// REMOVED (bd-c7af): Counter sync - no longer needed with hash IDs
totalDuration := time.Since(startTime)

View File

@@ -290,17 +290,7 @@ func renumberIssuesInDB(ctx context.Context, prefix string, idMapping map[string
}
// Update the counter to the highest renumbered ID so next issue gets correct number
// After renumbering to bd-1..bd-N, set counter to N so next issue is bd-(N+1)
// We need to FORCE set it (not MAX) because counter may be higher from deleted issues
// Strategy: Reset (delete) the counter row, then SyncAllCounters recreates it from actual max ID
sqliteStore, _ := store.(*sqlite.SQLiteStorage)
if err := sqliteStore.ResetCounter(ctx, prefix); err != nil {
return fmt.Errorf("failed to reset counter: %w", err)
}
// Now sync will recreate it from the actual max ID in the database
if err := sqliteStore.SyncAllCounters(ctx); err != nil {
return fmt.Errorf("failed to sync counter: %w", err)
}
// REMOVED (bd-c7af): Counter sync after renumbering - no longer needed with hash IDs
return nil
}

View File

@@ -9,7 +9,7 @@ This directory contains examples of how to integrate bd with AI agents and workf
- **[markdown-to-jsonl/](markdown-to-jsonl/)** - Convert markdown planning docs to bd issues
- **[github-import/](github-import/)** - Import issues from GitHub repositories
- **[git-hooks/](git-hooks/)** - Pre-configured git hooks for automatic export/import
- **[branch-merge/](branch-merge/)** - Branch merge workflow with collision resolution
<!-- REMOVED (bd-4c74): branch-merge example - collision resolution no longer needed with hash IDs -->
- **[claude-desktop-mcp/](claude-desktop-mcp/)** - MCP server for Claude Desktop integration
- **[claude-code-skill/](claude-code-skill/)** - Claude Code skill for effective beads usage patterns
@@ -28,9 +28,7 @@ cd bash-agent
cd git-hooks
./install.sh
# Try branch merge collision resolution
cd branch-merge
./demo.sh
# REMOVED (bd-4c74): branch-merge demo - hash IDs eliminate collision resolution
```
## Creating Your Own Agent

View File

@@ -1,176 +0,0 @@
# Branch Merge Workflow with Collision Resolution
This example demonstrates how to handle ID collisions when merging branches that have diverged and created issues with the same IDs.
## The Problem
When two branches work independently and both create issues, they'll often generate overlapping IDs:
```
main: bd-1, bd-2, bd-3, bd-4, bd-5
feature: bd-1, bd-2, bd-3, bd-6, bd-7 (diverged from main earlier)
```
When you try to merge `feature` into `main`, you'll have ID collisions for bd-1 through bd-3 (if the content differs).
## The Solution
bd provides automatic collision resolution that:
1. Detects collisions (same ID, different content)
2. Renumbers the incoming colliding issues
3. Updates ALL text references and dependencies automatically
## Demo Workflow
### 1. Setup - Two Diverged Branches
```bash
# Start on main branch
git checkout main
bd create "Feature A" -t feature -p 1
bd create "Bug fix B" -t bug -p 0
bd create "Task C" -t task -p 2
bd export -o .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -m "Add main branch issues"
# Create feature branch from an earlier commit
git checkout -b feature-branch HEAD~5
# On feature branch, create overlapping issues
bd create "Different feature A" -t feature -p 2
bd create "Different bug B" -t bug -p 1
bd create "Feature D" -t feature -p 1
bd export -o .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -m "Add feature branch issues"
```
At this point:
- `main` has: bd-1 (Feature A), bd-2 (Bug fix B), bd-3 (Task C)
- `feature-branch` has: bd-1 (Different feature A), bd-2 (Different bug B), bd-3 (Feature D)
The bd-1 and bd-2 on each branch have different content = collisions!
### 2. Merge and Detect Collisions
```bash
# Merge feature branch into main
git checkout main
git merge feature-branch
# Git will show merge conflict in .beads/issues.jsonl
# Manually resolve the conflict by keeping both versions
# (or use a merge tool)
# After resolving the git conflict, check for ID collisions
bd import -i .beads/issues.jsonl --dry-run
```
Output shows:
```
=== Collision Detection Report ===
Exact matches (idempotent): 0
New issues: 1
COLLISIONS DETECTED: 2
Colliding issues:
bd-1: Different feature A
Conflicting fields: [title, priority]
bd-2: Different bug B
Conflicting fields: [title, priority]
```
### 3. Resolve Collisions Automatically
```bash
# Let bd resolve the collisions
bd import -i .beads/issues.jsonl --resolve-collisions
```
Output shows:
```
Resolving collisions...
=== Remapping Report ===
Issues remapped: 2
Remappings (sorted by reference count):
bd-1 → bd-4 (refs: 0)
bd-2 → bd-5 (refs: 0)
All text and dependency references have been updated.
Import complete: 2 created, 0 updated, 1 dependencies added, 2 issues remapped
```
Result:
- `main` keeps: bd-1 (Feature A), bd-2 (Bug fix B), bd-3 (Task C)
- `feature-branch` issues become: bd-4 (Different feature A), bd-5 (Different bug B), bd-3 (Feature D)
### 4. Export and Commit
```bash
# Export the resolved state back to JSONL
bd export -o .beads/issues.jsonl
# Commit the merge
git add .beads/issues.jsonl
git commit -m "Merge feature-branch with collision resolution"
```
## Advanced: Cross-References
If your issues reference each other in text or dependencies, bd updates those automatically:
```bash
# On feature branch, create issues with references
bd create "Feature X" -d "Implements the core logic" -t feature -p 1
# Assume this becomes bd-10
bd create "Test for X" -d "Tests bd-10 functionality" -t task -p 2
# This references bd-10 in the description
bd dep add bd-11 bd-10 --type blocks
# Dependencies are created
# After merge with collision resolution
bd import -i .beads/issues.jsonl --resolve-collisions
# If bd-10 collided and was remapped to bd-15:
# - bd-11's description becomes: "Tests bd-15 functionality"
# - Dependency becomes: bd-11 → bd-15
```
## When to Use This
1. **Feature branches** - Long-lived branches that create issues independently
2. **Parallel development** - Multiple developers working on separate branches
3. **Stale branches** - Old branches that need to be merged but have ID conflicts
4. **Distributed teams** - Teams that work offline and sync via git
## Safety Notes
- `--resolve-collisions` preserves your existing database (current branch's issues never change IDs)
- Only the incoming colliding issues get new IDs
- Use `--dry-run` first to preview what will happen
- All text references use word-boundary matching (bd-10 won't match bd-100)
- The collision resolution is deterministic (same input = same output)
## Alternative: Manual Resolution
If you prefer manual control:
1. Don't use `--resolve-collisions`
2. Manually edit the JSONL file before import
3. Rename colliding IDs to unique values
4. Manually update any cross-references
5. Import normally
This gives you complete control but is more error-prone and time-consuming.
## See Also
- [Git Hooks Example](../git-hooks/) - Automate export/import with git hooks
- [README.md](../../README.md) - Full collision resolution documentation
- [TEXT_FORMATS.md](../../TEXT_FORMATS.md) - JSONL merge strategies

View File

@@ -1,145 +0,0 @@
#!/bin/bash
# Demo script for branch merge collision resolution workflow
# This script simulates a branch merge with ID collisions
set -e # Exit on error
echo "=== Branch Merge Collision Resolution Demo ==="
echo ""
# Check if bd is available
if ! command -v bd &> /dev/null; then
echo "Error: bd command not found. Please install bd first."
echo "Run: go install github.com/steveyegge/beads/cmd/bd@latest"
exit 1
fi
# Create a temporary directory for the demo
DEMO_DIR=$(mktemp -d -t bd-merge-demo-XXXXXX)
echo "Demo directory: $DEMO_DIR"
cd "$DEMO_DIR"
# Initialize git repo
echo ""
echo "Step 1: Initialize git repo and bd database"
git init
git config user.name "Demo User"
git config user.email "demo@example.com"
bd init --prefix demo
# Create initial commit
echo "Initial project" > README.txt
git add README.txt .beads/
git commit -m "Initial commit"
# Create issues on main branch
echo ""
echo "Step 2: Create issues on main branch"
bd create "Implement login" -d "User authentication system" -t feature -p 1 --json
bd create "Fix memory leak" -d "Memory leak in parser" -t bug -p 0 --json
bd create "Update docs" -d "Document new API" -t task -p 2 --json
echo ""
echo "Main branch issues:"
bd list
# Export and commit
bd export -o .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -m "Add main branch issues (bd-1, bd-2, bd-3)"
# Create feature branch from earlier point
echo ""
echo "Step 3: Create feature branch"
git checkout -b feature-branch HEAD~1
# Reimport to get clean state
bd import -i .beads/issues.jsonl
# Create overlapping issues on feature branch
echo ""
echo "Step 4: Create different issues with same IDs on feature branch"
bd create "Add dashboard" -d "Admin dashboard feature" -t feature -p 2 --json
bd create "Improve performance" -d "Optimize queries" -t task -p 1 --json
bd create "Add metrics" -d "Monitoring and metrics" -t feature -p 1 --json
echo ""
echo "Feature branch issues:"
bd list
# Export and commit
bd export -o .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -m "Add feature branch issues (bd-1, bd-2, bd-3)"
# Merge back to main
echo ""
echo "Step 5: Merge feature branch into main"
git checkout main
# Attempt merge (will conflict)
if git merge feature-branch --no-edit; then
echo "Merge succeeded without conflicts"
else
echo "Merge conflict detected - resolving..."
# Keep both versions by accepting both sides
# In a real scenario, you'd resolve this more carefully
git checkout --ours .beads/issues.jsonl
git checkout --theirs .beads/issues.jsonl --patch || true
# For demo purposes, accept theirs
git checkout --theirs .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -m "Merge feature-branch"
fi
# Detect collisions
echo ""
echo "Step 6: Detect ID collisions"
echo "Running: bd import -i .beads/issues.jsonl --dry-run"
echo ""
if bd import -i .beads/issues.jsonl --dry-run; then
echo "No collisions detected!"
else
echo ""
echo "Collisions detected (expected)!"
fi
# Resolve collisions
echo ""
echo "Step 7: Resolve collisions automatically"
echo "Running: bd import -i .beads/issues.jsonl --resolve-collisions"
echo ""
bd import -i .beads/issues.jsonl --resolve-collisions
# Show final state
echo ""
echo "Step 8: Final issue list after resolution"
bd list
# Show remapping details
echo ""
echo "Step 9: Show how dependencies and references are maintained"
echo "All text references like 'see bd-1' and dependencies were automatically updated!"
# Export final state
bd export -o .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -m "Resolve collisions and finalize merge"
echo ""
echo "=== Demo Complete ==="
echo ""
echo "Summary:"
echo "- Created issues on main branch (bd-1, bd-2, bd-3)"
echo "- Created different issues on feature branch (also bd-1, bd-2, bd-3)"
echo "- Merged branches with Git"
echo "- Detected collisions with --dry-run"
echo "- Resolved collisions with --resolve-collisions"
echo "- Feature branch issues were renumbered to avoid conflicts"
echo ""
echo "Demo directory: $DEMO_DIR"
echo "You can explore the git history: cd $DEMO_DIR && git log --oneline"
echo ""
echo "To clean up: rm -rf $DEMO_DIR"

View File

@@ -312,6 +312,11 @@ func handleRename(ctx context.Context, s *sqlite.SQLiteStorage, existing *types.
// The rename is already complete in the database
return deletedID, nil
}
// REMOVED (bd-8e05): Sequential ID collision handling during rename
// With hash-based IDs, rename collisions should not occur
return "", fmt.Errorf("rename collision handling removed - should not occur with hash IDs")
/* OLD CODE REMOVED (bd-8e05)
// Different content - this is a collision during rename
// Allocate a new ID for the incoming issue instead of using the desired ID
prefix, err := s.GetConfig(ctx, "issue_prefix")
@@ -324,13 +329,6 @@ func handleRename(ctx context.Context, s *sqlite.SQLiteStorage, existing *types.
// Retry up to 3 times to handle concurrent ID allocation
const maxRetries = 3
for attempt := 0; attempt < maxRetries; attempt++ {
// Sync counters before allocation to avoid collisions
if attempt > 0 {
if syncErr := s.SyncAllCounters(ctx); syncErr != nil {
return "", fmt.Errorf("failed to sync counters on retry %d: %w", attempt, syncErr)
}
}
newID, err := s.AllocateNextID(ctx, prefix)
if err != nil {
return "", fmt.Errorf("failed to generate new ID for rename collision: %w", err)
@@ -372,6 +370,7 @@ func handleRename(ctx context.Context, s *sqlite.SQLiteStorage, existing *types.
// This is acceptable because the old ID no longer exists in the system.
return oldID, nil
*/
}
// Check if old ID still exists (it might have been deleted by another clone)
@@ -563,10 +562,7 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues
result.Created += len(newIssues)
}
// Sync counters after batch import
if err := sqliteStore.SyncAllCounters(ctx); err != nil {
return fmt.Errorf("error syncing counters: %w", err)
}
// REMOVED (bd-c7af): Counter sync after import - no longer needed with hash IDs
return nil
}

View File

@@ -942,12 +942,7 @@ func (m *MemoryStorage) UnderlyingConn(ctx context.Context) (*sql.Conn, error) {
return nil, fmt.Errorf("UnderlyingConn not available in memory storage")
}
// SyncAllCounters is a no-op now that sequential IDs are removed (bd-aa744b).
// Kept for backward compatibility with existing code that calls it.
func (m *MemoryStorage) SyncAllCounters(ctx context.Context) error {
// No-op: hash IDs don't use counters
return nil
}
// REMOVED (bd-c7af): SyncAllCounters - no longer needed with hash IDs
// MarkIssueDirty marks an issue as dirty for export
func (m *MemoryStorage) MarkIssueDirty(ctx context.Context, issueID string) error {

View File

@@ -353,11 +353,7 @@ func RemapCollisions(ctx context.Context, s *SQLiteStorage, collisions []*Collis
return nil, err
}
if attempt < maxRetries-1 {
if syncErr := s.SyncAllCounters(ctx); syncErr != nil {
return nil, fmt.Errorf("retry %d: UNIQUE constraint error, counter sync failed: %w (original error: %v)", attempt+1, syncErr, err)
}
}
// REMOVED (bd-c7af): Counter sync on retry - no longer needed with hash IDs
}
return nil, fmt.Errorf("failed after %d retries due to UNIQUE constraint violations: %w", maxRetries, lastErr)
@@ -365,44 +361,27 @@ func RemapCollisions(ctx context.Context, s *SQLiteStorage, collisions []*Collis
// remapCollisionsOnce performs a single attempt at collision resolution.
// This is the actual implementation that RemapCollisions wraps with retry logic.
// REMOVED (bd-8e05): With hash-based IDs, collision remapping is no longer needed.
func remapCollisionsOnce(ctx context.Context, s *SQLiteStorage, collisions []*CollisionDetail, _ []*types.Issue) (map[string]string, error) {
idMapping := make(map[string]string)
// Sync counters before remapping to avoid ID collisions
if err := s.SyncAllCounters(ctx); err != nil {
return nil, fmt.Errorf("failed to sync ID counters: %w", err)
// With hash-based IDs, collisions should not occur. If they do, it's a bug.
if len(collisions) > 0 {
return nil, fmt.Errorf("collision remapping no longer supported with hash IDs - %d collisions detected", len(collisions))
}
return nil, nil
}
// Step 1: Collect ALL dependencies before any modifications
// This prevents CASCADE DELETE from losing dependency information
allDepsBeforeRemap, err := s.GetAllDependencyRecords(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get all dependencies: %w", err)
}
// OLD IMPLEMENTATION REMOVED (bd-8e05) - retained for reference during migration
// The original 250+ line function implemented sequential ID-based collision remapping
// which is obsolete with hash-based IDs
// Step 2: Process each collision based on which version should be remapped
for _, collision := range collisions {
// Skip collisions with nil issues (shouldn't happen but be defensive)
if collision.IncomingIssue == nil {
return nil, fmt.Errorf("collision %s has nil IncomingIssue", collision.ID)
}
if collision.ExistingIssue == nil {
return nil, fmt.Errorf("collision %s has nil ExistingIssue", collision.ID)
}
oldID := collision.ID
// Allocate new ID using atomic counter
prefix, err := s.GetConfig(ctx, "issue_prefix")
if err != nil || prefix == "" {
prefix = "bd"
}
nextID, err := s.getNextIDForPrefix(ctx, prefix)
if err != nil {
return nil, fmt.Errorf("failed to generate new ID for collision %s: %w", oldID, err)
}
newID := fmt.Sprintf("%s-%d", prefix, nextID)
// Stub out the old implementation to avoid compile errors
// The actual 250+ line implementation has been removed (bd-8e05)
func _OLD_remapCollisionsOnce_REMOVED(ctx context.Context, s *SQLiteStorage, collisions []*CollisionDetail, _ []*types.Issue) (map[string]string, error) {
// Original implementation removed - see git history before bd-8e05
return nil, nil
}
/* OLD CODE REMOVED (bd-8e05) - kept for git history reference
if collision.RemapIncoming {
// Incoming has higher hash -> remap incoming, keep existing
// Record mapping
@@ -498,6 +477,7 @@ func remapCollisionsOnce(ctx context.Context, s *SQLiteStorage, collisions []*Co
return idMapping, nil
}
END OF REMOVED CODE */
// updateReferences updates all text field references and dependency records
// to point to new IDs based on the idMapping

View File

@@ -556,32 +556,8 @@ func migrateContentHashColumn(db *sql.DB) error {
return nil
}
// getNextIDForPrefix atomically generates the next ID for a given prefix
// Uses the issue_counters table for atomic, cross-process ID generation
func (s *SQLiteStorage) getNextIDForPrefix(ctx context.Context, prefix string) (int, error) {
var nextID int
err := s.db.QueryRowContext(ctx, `
INSERT INTO issue_counters (prefix, last_id)
VALUES (?, 1)
ON CONFLICT(prefix) DO UPDATE SET
last_id = last_id + 1
RETURNING last_id
`, prefix).Scan(&nextID)
if err != nil {
return 0, fmt.Errorf("failed to generate next ID for prefix %s: %w", prefix, err)
}
return nextID, nil
}
// AllocateNextID generates the next issue ID for a given prefix.
// This is a public wrapper around getNextIDForPrefix for use by other packages.
func (s *SQLiteStorage) AllocateNextID(ctx context.Context, prefix string) (string, error) {
nextID, err := s.getNextIDForPrefix(ctx, prefix)
if err != nil {
return "", err
}
return fmt.Sprintf("%s-%d", prefix, nextID), nil
}
// REMOVED (bd-8e05): getNextIDForPrefix and AllocateNextID - sequential ID generation
// no longer needed with hash-based IDs
// getNextChildNumber atomically generates the next child number for a parent ID
// Uses the child_counters table for atomic, cross-process child ID generation
@@ -631,12 +607,7 @@ func (s *SQLiteStorage) GetNextChildID(ctx context.Context, parentID string) (st
return childID, nil
}
// SyncAllCounters is a no-op now that sequential IDs are removed (bd-aa744b).
// Kept for backward compatibility with existing code that calls it.
func (s *SQLiteStorage) SyncAllCounters(ctx context.Context) error {
// No-op: hash IDs don't use counters
return nil
}
// REMOVED (bd-c7af): SyncAllCounters - no longer needed with hash IDs
// REMOVED (bd-166): derivePrefixFromPath was causing duplicate issues with wrong prefix
// The database should ALWAYS have issue_prefix config set explicitly (by 'bd init' or auto-import)
@@ -1081,9 +1052,7 @@ func bulkMarkDirty(ctx context.Context, conn *sql.Conn, issues []*types.Issue) e
// }
//
// // After importing with explicit IDs, sync counters to prevent collisions
// if err := store.SyncAllCounters(ctx); err != nil {
// return err
// }
// REMOVED (bd-c7af): SyncAllCounters example - no longer needed with hash IDs
//
// Performance:
// - 100 issues: ~30ms (vs ~900ms with CreateIssue loop)
@@ -1727,8 +1696,8 @@ func (s *SQLiteStorage) DeleteIssue(ctx context.Context, id string) error {
return err
}
// Sync counters after deletion to keep them accurate
return s.SyncAllCounters(ctx)
// REMOVED (bd-c7af): Counter sync after deletion - no longer needed with hash IDs
return nil
}
// DeleteIssuesResult contains statistics about a batch deletion operation
@@ -1781,9 +1750,7 @@ func (s *SQLiteStorage) DeleteIssues(ctx context.Context, ids []string, cascade
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
if err := s.SyncAllCounters(ctx); err != nil {
return nil, fmt.Errorf("failed to sync counters after deletion: %w", err)
}
// REMOVED (bd-c7af): Counter sync after deletion - no longer needed with hash IDs
return result, nil
}