Skip to content

Commit 4091f87

Browse files
committed
Add cross-project-aware ID generation to Store
Add generateUniqueIDExcluding and CreateExcluding to the store layer. CreateExcluding generates IDs that are collision-free within both the local store and an external exclusion set, enabling cross-project ID uniqueness. Create delegates to CreateExcluding with a nil exclusion set so all existing callers are unaffected. Built with Raymond (Agent Orchestrator)
1 parent 36992f7 commit 4091f87

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

internal/store/store.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,28 @@ func (s *Store) save() error {
142142
// At IDMaxLen, it retries indefinitely.
143143
// Caller must hold s.mu.
144144
func (s *Store) generateUniqueID() string {
145+
return s.generateUniqueIDExcluding(nil)
146+
}
147+
148+
// generateUniqueIDExcluding creates a collision-free bead ID, also excluding
149+
// any IDs in the provided set (used for cross-project uniqueness).
150+
// Same retry/length-escalation logic as generateUniqueID.
151+
// Caller must hold s.mu.
152+
func (s *Store) generateUniqueIDExcluding(excluded map[string]struct{}) string {
145153
for n := model.IDMinLen; n <= model.IDMaxLen; n++ {
146154
retries := 3
147155
if n == model.IDMaxLen {
148156
retries = 100 // effectively unlimited at max length
149157
}
150158
for i := 0; i < retries; i++ {
151159
id := model.GenerateIDN(n)
152-
if _, exists := s.beads[id]; !exists {
153-
return id
160+
if _, exists := s.beads[id]; exists {
161+
continue
154162
}
163+
if _, inExcluded := excluded[id]; inExcluded {
164+
continue
165+
}
166+
return id
155167
}
156168
}
157169
// Should never reach here given 36^8 possible IDs.
@@ -161,11 +173,18 @@ func (s *Store) generateUniqueID() string {
161173
// Create adds a bead to the store and persists to disk.
162174
// If b.ID is empty, a collision-free ID is generated automatically.
163175
func (s *Store) Create(b model.Bead) (model.Bead, error) {
176+
return s.CreateExcluding(b, nil)
177+
}
178+
179+
// CreateExcluding adds a bead to the store and persists to disk.
180+
// If b.ID is empty, a collision-free ID is generated that is also absent from
181+
// the excluded set, enabling cross-project uniqueness.
182+
func (s *Store) CreateExcluding(b model.Bead, excluded map[string]struct{}) (model.Bead, error) {
164183
s.mu.Lock()
165184
defer s.mu.Unlock()
166185

167186
if b.ID == "" {
168-
b.ID = s.generateUniqueID()
187+
b.ID = s.generateUniqueIDExcluding(excluded)
169188
} else if _, exists := s.beads[b.ID]; exists {
170189
return model.Bead{}, fmt.Errorf("bead %s already exists", b.ID)
171190
}

internal/store/store_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,88 @@ func TestLoadMigratesMixedStatuses(t *testing.T) {
475475
}
476476
}
477477

478+
// --- CreateExcluding tests ---
479+
480+
func TestCreateExcludingNeverReturnsExcludedID(t *testing.T) {
481+
s, _ := Load(tempPath(t))
482+
483+
// Build a large exclusion set using IDs in the typical generated format.
484+
excluded := map[string]struct{}{
485+
"bd-aaaa": {},
486+
"bd-bbbb": {},
487+
"bd-cccc": {},
488+
"bd-dddd": {},
489+
"bd-eeee": {},
490+
}
491+
492+
for i := 0; i < 50; i++ {
493+
b := model.NewBead("Excluded test")
494+
created, err := s.CreateExcluding(b, excluded)
495+
if err != nil {
496+
t.Fatalf("iteration %d: unexpected error: %v", i, err)
497+
}
498+
if _, found := excluded[created.ID]; found {
499+
t.Fatalf("iteration %d: returned excluded ID %q", i, created.ID)
500+
}
501+
}
502+
}
503+
504+
func TestCreateExcludingWithNilExclusionSetBehavesLikeCreate(t *testing.T) {
505+
s, _ := Load(tempPath(t))
506+
507+
b := model.NewBead("Nil exclusion")
508+
created, err := s.CreateExcluding(b, nil)
509+
if err != nil {
510+
t.Fatalf("unexpected error: %v", err)
511+
}
512+
if created.ID == "" {
513+
t.Fatal("expected non-empty ID")
514+
}
515+
516+
// Verify it is retrievable.
517+
got, err := s.Get(created.ID)
518+
if err != nil {
519+
t.Fatalf("unexpected error getting bead: %v", err)
520+
}
521+
if got.Title != "Nil exclusion" {
522+
t.Errorf("expected title 'Nil exclusion', got %q", got.Title)
523+
}
524+
}
525+
526+
func TestCreateExcludingWithEmptyExclusionSetBehavesLikeCreate(t *testing.T) {
527+
s, _ := Load(tempPath(t))
528+
529+
b := model.NewBead("Empty exclusion")
530+
created, err := s.CreateExcluding(b, map[string]struct{}{})
531+
if err != nil {
532+
t.Fatalf("unexpected error: %v", err)
533+
}
534+
if created.ID == "" {
535+
t.Fatal("expected non-empty ID")
536+
}
537+
}
538+
539+
func TestCreateStillWorksAfterRefactor(t *testing.T) {
540+
s, _ := Load(tempPath(t))
541+
542+
b := model.NewBead("Create regression")
543+
created, err := s.Create(b)
544+
if err != nil {
545+
t.Fatalf("unexpected error: %v", err)
546+
}
547+
if created.ID == "" {
548+
t.Fatal("expected non-empty ID after Create")
549+
}
550+
551+
got, err := s.Get(created.ID)
552+
if err != nil {
553+
t.Fatalf("unexpected error getting bead: %v", err)
554+
}
555+
if got.Title != "Create regression" {
556+
t.Errorf("expected title 'Create regression', got %q", got.Title)
557+
}
558+
}
559+
478560
func TestAtomicWriteFileExists(t *testing.T) {
479561
path := tempPath(t)
480562
s, err := Load(path)

0 commit comments

Comments
 (0)