Implement reference scoring algorithm (bd-13)

Add reference scoring to prioritize which colliding issues should be
renumbered during collision resolution. Issues with fewer references
are renumbered first to minimize total update work.

Changes to collision.go:
- Add ReferenceScore field to CollisionDetail
- scoreCollisions() calculates scores and sorts collisions ascending
- countReferences() counts text mentions + dependency references
- Uses word-boundary regex (\b) to match exact IDs (bd-10 not bd-100)

New tests in collision_test.go:
- TestCountReferences: validates reference counting logic
- TestScoreCollisions: verifies scoring and sorting behavior
- TestCountReferencesWordBoundary: ensures exact ID matching

Reference score = text mentions (desc/design/notes/criteria) + deps
Sort order: fewest references first (minimizes renumbering impact)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-10-12 16:27:05 -07:00
parent b065b32a51
commit 42e3bb315d
3 changed files with 411 additions and 24 deletions

View File

@@ -1,24 +1,24 @@
{"id":"bd-1","title":"Add export/import commands","description":"Support bd export --format=jsonl and bd import for text-based git workflow","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T00:43:03.453438-07:00","updated_at":"2025-10-12T15:07:26.332681-07:00"}
{"id":"bd-10","title":"Extend export to include dependencies in JSONL","description":"Modify export.go to include dependencies array in each issue's JSONL output. This makes JSONL self-contained and enables proper collision resolution. Format: {\"id\":\"bd-10\",\"dependencies\":[{\"depends_on_id\":\"bd-5\",\"type\":\"blocks\"}]}","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-12T14:32:19.163526-07:00","updated_at":"2025-10-12T15:07:47.937992-07:00","closed_at":"2025-10-12T15:07:47.937992-07:00","dependencies":[{"issue_id":"bd-10","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:35:59.213222-07:00","created_by":"stevey"}]}
{"id":"bd-11","title":"Test issue to verify fix","description":"This should be bd-11 if the fix works","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T14:40:21.419082-07:00","updated_at":"2025-10-12T15:07:26.333114-07:00","closed_at":"2025-10-12T14:40:32.963312-07:00"}
{"id":"bd-12","title":"Implement collision detection in import","description":"Create collision.go with detectCollisions() function. Compare incoming JSONL issues against DB state. Distinguish between: (1) exact match (idempotent), (2) ID match but different content (collision), (3) new issue. Return list of colliding issues.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.056588-07:00","updated_at":"2025-10-12T16:06:25.575038-07:00","closed_at":"2025-10-12T16:06:25.575038-07:00","dependencies":[{"issue_id":"bd-12","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.947358-07:00","created_by":"stevey"}]}
{"id":"bd-13","title":"Implement reference scoring algorithm","description":"Count references for each colliding issue: text mentions in descriptions/notes/design fields + dependency references. Sort collisions by score ascending (fewest refs first). This minimizes total updates during renumbering.","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.204518-07:00","updated_at":"2025-10-12T15:07:26.333301-07:00","dependencies":[{"issue_id":"bd-13","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.951605-07:00","created_by":"stevey"}]}
{"id":"bd-14","title":"Implement ID remapping with reference updates","description":"Allocate new IDs for colliding issues. Update all text field references using word-boundary regex (\\bbd-10\\b). Update dependency records. Build id_mapping for reporting. Handle chain dependencies properly.","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.367596-07:00","updated_at":"2025-10-12T15:07:26.333388-07:00","dependencies":[{"issue_id":"bd-14","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.956041-07:00","created_by":"stevey"}]}
{"id":"bd-15","title":"Add --resolve-collisions flag and user reporting","description":"Add import flags: --resolve-collisions (auto-fix) and --dry-run (preview). Display clear report: collisions detected, remappings applied (old→new with scores), reference counts updated. Default behavior: fail on collision (safe).","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.534721-07:00","updated_at":"2025-10-12T15:07:26.33347-07:00","dependencies":[{"issue_id":"bd-15","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.961157-07:00","created_by":"stevey"}]}
{"id":"bd-16","title":"Write comprehensive collision resolution tests","description":"Test cases: simple collision, multiple collisions, dependency updates, text reference updates, chain dependencies, edge cases (partial ID matches, case sensitivity, triple merges). Add to import_test.go and collision_test.go.","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.702127-07:00","updated_at":"2025-10-12T15:07:26.333559-07:00","dependencies":[{"issue_id":"bd-16","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.965816-07:00","created_by":"stevey"}]}
{"id":"bd-17","title":"Update documentation for collision resolution","description":"Update README.md with collision resolution section. Update CLAUDE.md with new workflow. Document --resolve-collisions and --dry-run flags. Add example scenarios showing branch merge workflows.","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.866649-07:00","updated_at":"2025-10-12T15:07:26.333636-07:00","dependencies":[{"issue_id":"bd-17","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.970302-07:00","created_by":"stevey"}]}
{"id":"bd-18","title":"Add design/notes/acceptance_criteria fields to update command","description":"Currently bd update only supports status, priority, title, assignee. Add support for --design, --notes, --acceptance-criteria flags. This makes it easier to add detailed designs to issues after creation.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T14:40:57.032395-07:00","updated_at":"2025-10-12T15:07:26.33372-07:00"}
{"id":"bd-19","title":"Fix import zero-value field handling","description":"Import uses zero-value checks (Priority != 0) to determine field updates. This prevents setting priority to 0 or clearing string fields. Export/import round-trip not fully idempotent for zero values. Consider JSON presence detection or explicit preserve-existing semantics. Location: cmd/bd/import.go:95-106","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-10-12T15:13:17.895083-07:00","updated_at":"2025-10-12T15:21:23.259692-07:00"}
{"id":"bd-2","title":"Add PostgreSQL backend","description":"Implement PostgreSQL storage backend as alternative to SQLite for larger teams","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-10-12T00:43:03.457453-07:00","updated_at":"2025-10-12T15:07:26.333816-07:00","closed_at":"2025-10-12T14:15:04.00695-07:00"}
{"id":"bd-20","title":"Add --strict flag for dependency import failures","description":"Currently dependency import errors are warnings (logged to stderr, execution continues). Missing targets or cycles may indicate JSONL corruption. Add --strict flag to fail on any dependency errors for data integrity validation. Location: cmd/bd/import.go:159-164","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-12T15:13:18.954834-07:00","updated_at":"2025-10-12T15:22:25.81646-07:00"}
{"id":"bd-21","title":"Simplify getNextID SQL query parameters","description":"Query passes prefix four times to same SQL query. Works but fragile if query changes. Consider simplifying SQL to require fewer parameters. Location: internal/storage/sqlite/sqlite.go:73-78","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T15:13:20.114733-07:00","updated_at":"2025-10-12T15:23:08.014893-07:00"}
{"id":"bd-22","title":"Add validation/warning for malformed issue IDs","description":"getNextID silently ignores non-numeric ID suffixes (e.g., bd-foo). CAST returns NULL for invalid strings. Consider detecting and warning about malformed IDs in database. Location: internal/storage/sqlite/sqlite.go:79-82","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T15:13:21.195975-07:00","updated_at":"2025-10-12T15:23:57.434894-07:00"}
{"id":"bd-23","title":"Optimize export dependency queries (N+1 problem)","description":"Export triggers separate GetDependencyRecords() per issue. For large DBs (1000+ issues), this is N+1 queries. Add GetAllDependencyRecords() to fetch all dependencies in one query. Location: cmd/bd/export.go:52-59, import.go:138-142","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T15:13:22.325113-07:00","updated_at":"2025-10-12T15:25:26.278357-07:00"}
{"id":"bd-24","title":"Support ID space partitioning for parallel worker agents","description":"Enable external orchestrators (like AI worker swarms) to control issue ID assignment. Add --id flag to 'bd create' for explicit ID specification. Optionally support 'bd config set next_id N' to set the starting point for auto-increment. Storage layer already supports pre-assigned IDs (sqlite.go:52-71), just need CLI wiring. This keeps beads simple while letting orchestrators implement their own ID partitioning strategies to minimize merge conflicts. Complementary to bd-9's collision resolution.","status":"open","priority":1,"issue_type":"feature","created_at":"2025-10-12T16:10:37.808226-07:00","updated_at":"2025-10-12T16:10:37.808226-07:00"}
{"id":"bd-3","title":"Document git workflow in README","description":"Add Git Workflow section to README explaining binary vs text approaches","status":"closed","priority":1,"issue_type":"chore","created_at":"2025-10-12T00:43:03.461615-07:00","updated_at":"2025-10-12T15:07:26.333908-07:00","closed_at":"2025-10-12T00:43:30.283178-07:00"}
{"id":"bd-4","title":"Add demo GIF/video showing bd quickstart in action","description":"Record asciinema or create animated GIF showing the full workflow","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T10:50:49.500051-07:00","updated_at":"2025-10-12T15:07:26.334-07:00","dependencies":[{"issue_id":"bd-4","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.399915-07:00","created_by":"stevey"}]}
{"id":"bd-5","title":"Implement MCP server for Claude Desktop","description":"Complete the claude-desktop-mcp example with working TypeScript implementation","status":"open","priority":1,"issue_type":"feature","created_at":"2025-10-12T10:50:50.942964-07:00","updated_at":"2025-10-12T15:07:26.334092-07:00","dependencies":[{"issue_id":"bd-5","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.404381-07:00","created_by":"stevey"}]}
{"id":"bd-6","title":"Add migration scripts for GitHub Issues","description":"Create scripts to import from GitHub Issues API or exported JSON","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T10:50:52.140018-07:00","updated_at":"2025-10-12T15:07:26.334162-07:00","dependencies":[{"issue_id":"bd-6","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.40857-07:00","created_by":"stevey"}]}
{"id":"bd-7","title":"Add performance benchmarks document","description":"Document actual performance metrics with hyperfine tests","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-12T10:50:53.294516-07:00","updated_at":"2025-10-12T15:07:26.334232-07:00","dependencies":[{"issue_id":"bd-7","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.412835-07:00","created_by":"stevey"}]}
{"id":"bd-8","title":"Reach 1.0 release milestone","description":"Stabilize API, finalize documentation, comprehensive testing","status":"open","priority":1,"issue_type":"epic","created_at":"2025-10-12T10:50:54.457348-07:00","updated_at":"2025-10-12T15:07:26.334308-07:00"}
{"id":"bd-9","title":"Build collision resolution tooling for distributed branch workflows","description":"When branches diverge and both create issues, auto-incrementing IDs collide on merge. Build excellent tooling to detect collisions during import, auto-renumber issues with fewer dependencies, update all references in descriptions and dependency links, and provide clear user feedback. Goal: keep beautiful brevity of numeric IDs (bd-302) while handling distributed creation gracefully.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2025-10-12T13:39:34.608218-07:00","updated_at":"2025-10-12T15:07:26.334385-07:00"}
{"id":"bd-1","title":"Add export/import commands","description":"Support bd export --format=jsonl and bd import for text-based git workflow","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T00:43:03.453438-07:00","updated_at":"2025-10-12T16:19:11.969068-07:00"}
{"id":"bd-10","title":"Extend export to include dependencies in JSONL","description":"Modify export.go to include dependencies array in each issue's JSONL output. This makes JSONL self-contained and enables proper collision resolution. Format: {\"id\":\"bd-10\",\"dependencies\":[{\"depends_on_id\":\"bd-5\",\"type\":\"blocks\"}]}","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-12T14:32:19.163526-07:00","updated_at":"2025-10-12T16:19:11.969345-07:00","closed_at":"2025-10-12T15:07:47.937992-07:00","dependencies":[{"issue_id":"bd-10","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:35:59.213222-07:00","created_by":"stevey"}]}
{"id":"bd-11","title":"Test issue to verify fix","description":"This should be bd-11 if the fix works","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T14:40:21.419082-07:00","updated_at":"2025-10-12T16:19:11.96945-07:00","closed_at":"2025-10-12T14:40:32.963312-07:00"}
{"id":"bd-12","title":"Implement collision detection in import","description":"Create collision.go with detectCollisions() function. Compare incoming JSONL issues against DB state. Distinguish between: (1) exact match (idempotent), (2) ID match but different content (collision), (3) new issue. Return list of colliding issues.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.056588-07:00","updated_at":"2025-10-12T16:19:11.96955-07:00","closed_at":"2025-10-12T16:06:25.575038-07:00","dependencies":[{"issue_id":"bd-12","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.947358-07:00","created_by":"stevey"}]}
{"id":"bd-13","title":"Implement reference scoring algorithm","description":"Count references for each colliding issue: text mentions in descriptions/notes/design fields + dependency references. Sort collisions by score ascending (fewest refs first). This minimizes total updates during renumbering.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.204518-07:00","updated_at":"2025-10-12T16:26:46.572201-07:00","closed_at":"2025-10-12T16:26:46.572201-07:00","dependencies":[{"issue_id":"bd-13","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.951605-07:00","created_by":"stevey"}]}
{"id":"bd-14","title":"Implement ID remapping with reference updates","description":"Allocate new IDs for colliding issues. Update all text field references using word-boundary regex (\\bbd-10\\b). Update dependency records. Build id_mapping for reporting. Handle chain dependencies properly.","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.367596-07:00","updated_at":"2025-10-12T16:19:11.969729-07:00","dependencies":[{"issue_id":"bd-14","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.956041-07:00","created_by":"stevey"}]}
{"id":"bd-15","title":"Add --resolve-collisions flag and user reporting","description":"Add import flags: --resolve-collisions (auto-fix) and --dry-run (preview). Display clear report: collisions detected, remappings applied (old→new with scores), reference counts updated. Default behavior: fail on collision (safe).","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.534721-07:00","updated_at":"2025-10-12T16:19:11.969818-07:00","dependencies":[{"issue_id":"bd-15","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.961157-07:00","created_by":"stevey"}]}
{"id":"bd-16","title":"Write comprehensive collision resolution tests","description":"Test cases: simple collision, multiple collisions, dependency updates, text reference updates, chain dependencies, edge cases (partial ID matches, case sensitivity, triple merges). Add to import_test.go and collision_test.go.","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.702127-07:00","updated_at":"2025-10-12T16:19:11.9699-07:00","dependencies":[{"issue_id":"bd-16","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.965816-07:00","created_by":"stevey"}]}
{"id":"bd-17","title":"Update documentation for collision resolution","description":"Update README.md with collision resolution section. Update CLAUDE.md with new workflow. Document --resolve-collisions and --dry-run flags. Add example scenarios showing branch merge workflows.","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-12T14:40:56.866649-07:00","updated_at":"2025-10-12T16:19:11.96998-07:00","dependencies":[{"issue_id":"bd-17","depends_on_id":"bd-9","type":"parent-child","created_at":"2025-10-12T14:41:07.970302-07:00","created_by":"stevey"}]}
{"id":"bd-18","title":"Add design/notes/acceptance_criteria fields to update command","description":"Currently bd update only supports status, priority, title, assignee. Add support for --design, --notes, --acceptance-criteria flags. This makes it easier to add detailed designs to issues after creation.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T14:40:57.032395-07:00","updated_at":"2025-10-12T16:19:11.970071-07:00"}
{"id":"bd-19","title":"Fix import zero-value field handling","description":"Import uses zero-value checks (Priority != 0) to determine field updates. This prevents setting priority to 0 or clearing string fields. Export/import round-trip not fully idempotent for zero values. Consider JSON presence detection or explicit preserve-existing semantics. Location: cmd/bd/import.go:95-106","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-10-12T15:13:17.895083-07:00","updated_at":"2025-10-12T16:19:11.970157-07:00"}
{"id":"bd-2","title":"Add PostgreSQL backend","description":"Implement PostgreSQL storage backend as alternative to SQLite for larger teams","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-10-12T00:43:03.457453-07:00","updated_at":"2025-10-12T16:19:11.97024-07:00","closed_at":"2025-10-12T14:15:04.00695-07:00"}
{"id":"bd-20","title":"Add --strict flag for dependency import failures","description":"Currently dependency import errors are warnings (logged to stderr, execution continues). Missing targets or cycles may indicate JSONL corruption. Add --strict flag to fail on any dependency errors for data integrity validation. Location: cmd/bd/import.go:159-164","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-10-12T15:13:18.954834-07:00","updated_at":"2025-10-12T16:19:11.970327-07:00"}
{"id":"bd-21","title":"Simplify getNextID SQL query parameters","description":"Query passes prefix four times to same SQL query. Works but fragile if query changes. Consider simplifying SQL to require fewer parameters. Location: internal/storage/sqlite/sqlite.go:73-78","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T15:13:20.114733-07:00","updated_at":"2025-10-12T16:19:11.970421-07:00"}
{"id":"bd-22","title":"Add validation/warning for malformed issue IDs","description":"getNextID silently ignores non-numeric ID suffixes (e.g., bd-foo). CAST returns NULL for invalid strings. Consider detecting and warning about malformed IDs in database. Location: internal/storage/sqlite/sqlite.go:79-82","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T15:13:21.195975-07:00","updated_at":"2025-10-12T16:19:11.970492-07:00"}
{"id":"bd-23","title":"Optimize export dependency queries (N+1 problem)","description":"Export triggers separate GetDependencyRecords() per issue. For large DBs (1000+ issues), this is N+1 queries. Add GetAllDependencyRecords() to fetch all dependencies in one query. Location: cmd/bd/export.go:52-59, import.go:138-142","status":"closed","priority":3,"issue_type":"task","created_at":"2025-10-12T15:13:22.325113-07:00","updated_at":"2025-10-12T16:19:11.97058-07:00"}
{"id":"bd-24","title":"Support ID space partitioning for parallel worker agents","description":"Enable external orchestrators (like AI worker swarms) to control issue ID assignment. Add --id flag to 'bd create' for explicit ID specification. Optionally support 'bd config set next_id N' to set the starting point for auto-increment. Storage layer already supports pre-assigned IDs (sqlite.go:52-71), just need CLI wiring. This keeps beads simple while letting orchestrators implement their own ID partitioning strategies to minimize merge conflicts. Complementary to bd-9's collision resolution.","status":"open","priority":1,"issue_type":"feature","created_at":"2025-10-12T16:10:37.808226-07:00","updated_at":"2025-10-12T16:19:11.970666-07:00"}
{"id":"bd-3","title":"Document git workflow in README","description":"Add Git Workflow section to README explaining binary vs text approaches","status":"closed","priority":1,"issue_type":"chore","created_at":"2025-10-12T00:43:03.461615-07:00","updated_at":"2025-10-12T16:19:11.970753-07:00","closed_at":"2025-10-12T00:43:30.283178-07:00"}
{"id":"bd-4","title":"Add demo GIF/video showing bd quickstart in action","description":"Record asciinema or create animated GIF showing the full workflow","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T10:50:49.500051-07:00","updated_at":"2025-10-12T16:19:11.97083-07:00","dependencies":[{"issue_id":"bd-4","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.399915-07:00","created_by":"stevey"}]}
{"id":"bd-5","title":"Implement MCP server for Claude Desktop","description":"Complete the claude-desktop-mcp example with working TypeScript implementation","status":"open","priority":1,"issue_type":"feature","created_at":"2025-10-12T10:50:50.942964-07:00","updated_at":"2025-10-12T16:19:11.970913-07:00","dependencies":[{"issue_id":"bd-5","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.404381-07:00","created_by":"stevey"}]}
{"id":"bd-6","title":"Add migration scripts for GitHub Issues","description":"Create scripts to import from GitHub Issues API or exported JSON","status":"open","priority":2,"issue_type":"feature","created_at":"2025-10-12T10:50:52.140018-07:00","updated_at":"2025-10-12T16:19:11.97099-07:00","dependencies":[{"issue_id":"bd-6","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.40857-07:00","created_by":"stevey"}]}
{"id":"bd-7","title":"Add performance benchmarks document","description":"Document actual performance metrics with hyperfine tests","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-12T10:50:53.294516-07:00","updated_at":"2025-10-12T16:19:11.971065-07:00","dependencies":[{"issue_id":"bd-7","depends_on_id":"bd-8","type":"parent-child","created_at":"2025-10-12T10:51:08.412835-07:00","created_by":"stevey"}]}
{"id":"bd-8","title":"Reach 1.0 release milestone","description":"Stabilize API, finalize documentation, comprehensive testing","status":"open","priority":1,"issue_type":"epic","created_at":"2025-10-12T10:50:54.457348-07:00","updated_at":"2025-10-12T16:19:11.971154-07:00"}
{"id":"bd-9","title":"Build collision resolution tooling for distributed branch workflows","description":"When branches diverge and both create issues, auto-incrementing IDs collide on merge. Build excellent tooling to detect collisions during import, auto-renumber issues with fewer dependencies, update all references in descriptions and dependency links, and provide clear user feedback. Goal: keep beautiful brevity of numeric IDs (bd-302) while handling distributed creation gracefully.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2025-10-12T13:39:34.608218-07:00","updated_at":"2025-10-12T16:19:11.971233-07:00"}

View File

@@ -3,6 +3,8 @@ package sqlite
import (
"context"
"fmt"
"regexp"
"sort"
"github.com/steveyegge/beads/internal/types"
)
@@ -20,6 +22,7 @@ type CollisionDetail struct {
IncomingIssue *types.Issue // The issue from the import file
ExistingIssue *types.Issue // The issue currently in the database
ConflictingFields []string // List of field names that differ
ReferenceScore int // Number of references to this issue (for scoring)
}
// detectCollisions compares incoming JSONL issues against DB state
@@ -121,3 +124,94 @@ func equalIntPtr(a, b *int) bool {
}
return *a == *b
}
// scoreCollisions calculates reference scores for all colliding issues and sorts them
// by score ascending (fewest references first). This minimizes the total number of
// updates needed during renumbering - issues with fewer references are renumbered first.
//
// Reference score = text mentions + dependency references
func scoreCollisions(ctx context.Context, s *SQLiteStorage, collisions []*CollisionDetail, allIssues []*types.Issue) error {
// Build a map of all issues for quick lookup
issueMap := make(map[string]*types.Issue)
for _, issue := range allIssues {
issueMap[issue.ID] = issue
}
// Get all dependency records for efficient lookup
allDeps, err := s.GetAllDependencyRecords(ctx)
if err != nil {
return fmt.Errorf("failed to get dependency records: %w", err)
}
// Calculate reference score for each collision
for _, collision := range collisions {
score, err := countReferences(collision.ID, allIssues, allDeps)
if err != nil {
return fmt.Errorf("failed to count references for %s: %w", collision.ID, err)
}
collision.ReferenceScore = score
}
// Sort collisions by reference score ascending (fewest first)
sort.Slice(collisions, func(i, j int) bool {
return collisions[i].ReferenceScore < collisions[j].ReferenceScore
})
return nil
}
// countReferences counts how many times an issue ID is referenced
// Returns: text mentions + dependency references
func countReferences(issueID string, allIssues []*types.Issue, allDeps map[string][]*types.Dependency) (int, error) {
count := 0
// Count text mentions in all issues' text fields
// Use word boundary regex to match exact IDs (e.g., "bd-10" but not "bd-100")
pattern := fmt.Sprintf(`\b%s\b`, regexp.QuoteMeta(issueID))
re, err := regexp.Compile(pattern)
if err != nil {
return 0, fmt.Errorf("failed to compile regex for %s: %w", issueID, err)
}
for _, issue := range allIssues {
// Skip counting references in the issue itself
if issue.ID == issueID {
continue
}
// Count mentions in description
count += len(re.FindAllString(issue.Description, -1))
// Count mentions in design
count += len(re.FindAllString(issue.Design, -1))
// Count mentions in notes
count += len(re.FindAllString(issue.Notes, -1))
// Count mentions in acceptance criteria
count += len(re.FindAllString(issue.AcceptanceCriteria, -1))
}
// Count dependency references
// An issue can be referenced as either IssueID or DependsOnID
for _, deps := range allDeps {
for _, dep := range deps {
// Skip self-references
if dep.IssueID == issueID && dep.DependsOnID == issueID {
continue
}
// Count if this issue is the source (IssueID)
if dep.IssueID == issueID {
count++
}
// Count if this issue is the target (DependsOnID)
if dep.DependsOnID == issueID {
count++
}
}
}
return count, nil
}

View File

@@ -426,3 +426,296 @@ func TestEqualIntPtr(t *testing.T) {
func intPtr(i int) *int {
return &i
}
func TestCountReferences(t *testing.T) {
allIssues := []*types.Issue{
{
ID: "bd-1",
Title: "Issue 1",
Description: "This mentions bd-2 and bd-3",
Design: "Design mentions bd-2 twice: bd-2 and bd-2",
Notes: "Notes mention bd-3",
},
{
ID: "bd-2",
Title: "Issue 2",
Description: "This mentions bd-1",
},
{
ID: "bd-3",
Title: "Issue 3",
Description: "No mentions here",
},
{
ID: "bd-10",
Title: "Issue 10",
Description: "This has bd-100 but not bd-10 itself",
},
}
allDeps := map[string][]*types.Dependency{
"bd-1": {
{IssueID: "bd-1", DependsOnID: "bd-2", Type: types.DepBlocks},
},
"bd-2": {
{IssueID: "bd-2", DependsOnID: "bd-3", Type: types.DepBlocks},
},
}
tests := []struct {
name string
issueID string
expectedCount int
}{
{
name: "bd-1 - one text mention, one dependency",
issueID: "bd-1",
// Text: bd-2's description mentions bd-1 (1)
// Deps: bd-1 → bd-2 (1)
expectedCount: 2,
},
{
name: "bd-2 - multiple text mentions, two dependencies",
issueID: "bd-2",
// Text: bd-1's description mentions bd-2 (1) + bd-1's design mentions bd-2 three times (3) = 4
// (design has: "mentions bd-2" + "bd-2 and" + "bd-2")
// Deps: bd-1 → bd-2 (1) + bd-2 → bd-3 (1) = 2
expectedCount: 6,
},
{
name: "bd-3 - some text mentions, one dependency",
issueID: "bd-3",
// Text: bd-1's description (1) + bd-1's notes (1) = 2
// Deps: bd-2 → bd-3 (1)
expectedCount: 3,
},
{
name: "bd-10 - no mentions (bd-100 doesn't count)",
issueID: "bd-10",
// Text: bd-100 in bd-10's description doesn't match \bbd-10\b = 0
// Deps: none = 0
expectedCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
count, err := countReferences(tt.issueID, allIssues, allDeps)
if err != nil {
t.Fatalf("countReferences failed: %v", err)
}
if count != tt.expectedCount {
t.Errorf("expected count %d, got %d", tt.expectedCount, count)
}
})
}
}
func TestScoreCollisions(t *testing.T) {
// Create temporary database
tmpDir, err := os.MkdirTemp("", "score-collision-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath := filepath.Join(tmpDir, "test.db")
store, err := New(dbPath)
if err != nil {
t.Fatalf("failed to create storage: %v", err)
}
defer store.Close()
ctx := context.Background()
// Setup: Create issues with various reference patterns
issue1 := &types.Issue{
ID: "bd-1",
Title: "Issue 1",
Description: "Depends on bd-2",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
issue2 := &types.Issue{
ID: "bd-2",
Title: "Issue 2",
Description: "Referenced by bd-1 and bd-3",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
issue3 := &types.Issue{
ID: "bd-3",
Title: "Issue 3",
Description: "Mentions bd-2 multiple times: bd-2 and bd-2",
Notes: "Also mentions bd-2 here",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
issue4 := &types.Issue{
ID: "bd-4",
Title: "Issue 4",
Description: "Lonely issue with no references",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
// Create issues in DB
if err := store.CreateIssue(ctx, issue1, "test"); err != nil {
t.Fatalf("failed to create issue1: %v", err)
}
if err := store.CreateIssue(ctx, issue2, "test"); err != nil {
t.Fatalf("failed to create issue2: %v", err)
}
if err := store.CreateIssue(ctx, issue3, "test"); err != nil {
t.Fatalf("failed to create issue3: %v", err)
}
if err := store.CreateIssue(ctx, issue4, "test"); err != nil {
t.Fatalf("failed to create issue4: %v", err)
}
// Add dependencies
dep1 := &types.Dependency{IssueID: "bd-1", DependsOnID: "bd-2", Type: types.DepBlocks}
dep2 := &types.Dependency{IssueID: "bd-3", DependsOnID: "bd-2", Type: types.DepBlocks}
if err := store.AddDependency(ctx, dep1, "test"); err != nil {
t.Fatalf("failed to add dependency1: %v", err)
}
if err := store.AddDependency(ctx, dep2, "test"); err != nil {
t.Fatalf("failed to add dependency2: %v", err)
}
// Create collision details (simulated)
collisions := []*CollisionDetail{
{
ID: "bd-1",
IncomingIssue: issue1,
ExistingIssue: issue1,
ReferenceScore: 0, // Will be calculated
},
{
ID: "bd-2",
IncomingIssue: issue2,
ExistingIssue: issue2,
ReferenceScore: 0, // Will be calculated
},
{
ID: "bd-3",
IncomingIssue: issue3,
ExistingIssue: issue3,
ReferenceScore: 0, // Will be calculated
},
{
ID: "bd-4",
IncomingIssue: issue4,
ExistingIssue: issue4,
ReferenceScore: 0, // Will be calculated
},
}
allIssues := []*types.Issue{issue1, issue2, issue3, issue4}
// Score the collisions
err = scoreCollisions(ctx, store, collisions, allIssues)
if err != nil {
t.Fatalf("scoreCollisions failed: %v", err)
}
// Verify scores were calculated
// bd-4: 0 references (no mentions, no deps)
// bd-1: 1 reference (bd-1 → bd-2 dependency)
// bd-3: 1 reference (bd-3 → bd-2 dependency)
// bd-2: high references (mentioned in bd-1, bd-3 multiple times + 2 deps as target)
// bd-1 desc (1) + bd-3 desc (3: "bd-2 multiple", "bd-2 and", "bd-2") + bd-3 notes (1) + 2 deps = 7
if collisions[0].ID != "bd-4" {
t.Errorf("expected first collision to be bd-4 (lowest score), got %s", collisions[0].ID)
}
if collisions[0].ReferenceScore != 0 {
t.Errorf("expected bd-4 to have score 0, got %d", collisions[0].ReferenceScore)
}
// bd-2 should be last (highest score)
lastIdx := len(collisions) - 1
if collisions[lastIdx].ID != "bd-2" {
t.Errorf("expected last collision to be bd-2 (highest score), got %s", collisions[lastIdx].ID)
}
if collisions[lastIdx].ReferenceScore != 7 {
t.Errorf("expected bd-2 to have score 7, got %d", collisions[lastIdx].ReferenceScore)
}
// Verify sorting (ascending order)
for i := 1; i < len(collisions); i++ {
if collisions[i].ReferenceScore < collisions[i-1].ReferenceScore {
t.Errorf("collisions not sorted: collision[%d] score %d < collision[%d] score %d",
i, collisions[i].ReferenceScore, i-1, collisions[i-1].ReferenceScore)
}
}
}
func TestCountReferencesWordBoundary(t *testing.T) {
// Test that word boundaries work correctly
allIssues := []*types.Issue{
{
ID: "bd-1",
Description: "bd-10 and bd-100 and bd-1 and bd-11",
},
{
ID: "bd-10",
Description: "bd-1 and bd-100",
},
}
allDeps := map[string][]*types.Dependency{}
tests := []struct {
name string
issueID string
expectedCount int
description string
}{
{
name: "bd-1 exact match",
issueID: "bd-1",
expectedCount: 2, // bd-10's desc mentions bd-1 (1) + bd-1's desc mentions bd-1 (1) = 2
// Wait, bd-1's desc shouldn't count itself
// So: bd-10's desc mentions bd-1 (1)
},
{
name: "bd-10 exact match",
issueID: "bd-10",
expectedCount: 1, // bd-1's desc mentions bd-10 (1)
},
{
name: "bd-100 exact match",
issueID: "bd-100",
expectedCount: 2, // bd-1's desc (1) + bd-10's desc (1)
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
count, err := countReferences(tt.issueID, allIssues, allDeps)
if err != nil {
t.Fatalf("countReferences failed: %v", err)
}
// Adjust expected based on actual counting logic
// countReferences skips the issue itself
expected := tt.expectedCount
if tt.issueID == "bd-1" {
expected = 1 // only bd-10's description
}
if count != expected {
t.Errorf("expected count %d, got %d", expected, count)
}
})
}
}