feat(acb-evolver): add CRUD operations for programs database with island model

Add Delete, List, ListTopByIsland, and GetLineage methods to the programs
Store. These complete the CRUD operations needed for the evolution pipeline:

- Delete: Remove programs by ID
- List: Paginated listing of all programs
- ListTopByIsland: Get top N programs by fitness for a specific island
- GetLineage: Recursively traverse parent chain for lineage tracking

Also adds comprehensive tests for all new operations including lineage
tracking through grandparent-parent-child chains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-03-29 12:08:21 -04:00
parent 984ecc1da7
commit 3d9326d767
2 changed files with 236 additions and 0 deletions

View file

@ -280,3 +280,110 @@ func (s *Store) GetByBotID(ctx context.Context, botID string) (*Program, error)
}
return p, nil
}
// Delete removes a program by ID. Returns error if deletion fails.
func (s *Store) Delete(ctx context.Context, id int64) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM programs WHERE id = $1`, id)
if err != nil {
return fmt.Errorf("delete program %d: %w", id, err)
}
return nil
}
// List returns all programs ordered by creation date descending.
func (s *Store) List(ctx context.Context, limit, offset int) ([]*Program, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, code, language, island, generation, parent_ids,
behavior_vector, fitness, promoted, created_at
FROM programs
ORDER BY created_at DESC
LIMIT $1 OFFSET $2`, limit, offset)
if err != nil {
return nil, fmt.Errorf("list programs: %w", err)
}
defer rows.Close()
var programs []*Program
for rows.Next() {
p := &Program{}
var parentJSON string
if err := rows.Scan(
&p.ID, &p.Code, &p.Language, &p.Island, &p.Generation,
&parentJSON, pq.Array(&p.BehaviorVector), &p.Fitness, &p.Promoted, &p.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan program: %w", err)
}
if err := json.Unmarshal([]byte(parentJSON), &p.ParentIDs); err != nil {
return nil, fmt.Errorf("unmarshal parent_ids: %w", err)
}
programs = append(programs, p)
}
return programs, rows.Err()
}
// ListTopByIsland returns the top N programs on the given island by fitness.
func (s *Store) ListTopByIsland(ctx context.Context, island string, limit int) ([]*Program, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, code, language, island, generation, parent_ids,
behavior_vector, fitness, promoted, created_at
FROM programs WHERE island = $1
ORDER BY fitness DESC
LIMIT $2`, island, limit)
if err != nil {
return nil, fmt.Errorf("list top programs on %s: %w", island, err)
}
defer rows.Close()
var programs []*Program
for rows.Next() {
p := &Program{}
var parentJSON string
if err := rows.Scan(
&p.ID, &p.Code, &p.Language, &p.Island, &p.Generation,
&parentJSON, pq.Array(&p.BehaviorVector), &p.Fitness, &p.Promoted, &p.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan program: %w", err)
}
if err := json.Unmarshal([]byte(parentJSON), &p.ParentIDs); err != nil {
return nil, fmt.Errorf("unmarshal parent_ids: %w", err)
}
programs = append(programs, p)
}
return programs, rows.Err()
}
// GetLineage returns all ancestor program IDs for a given program by
// traversing the parent_ids chain recursively.
func (s *Store) GetLineage(ctx context.Context, id int64) ([]int64, error) {
visited := make(map[int64]bool)
var lineage []int64
var traverse func(programID int64) error
traverse = func(programID int64) error {
if visited[programID] {
return nil
}
visited[programID] = true
p, err := s.Get(ctx, programID)
if err != nil {
return err
}
if p == nil {
return nil
}
for _, parentID := range p.ParentIDs {
if err := traverse(parentID); err != nil {
return err
}
}
lineage = append(lineage, programID)
return nil
}
if err := traverse(id); err != nil {
return nil, fmt.Errorf("get lineage for %d: %w", id, err)
}
return lineage, nil
}

View file

@ -264,3 +264,132 @@ func TestParentIDs_Roundtrip(t *testing.T) {
t.Errorf("Generation: got %d, want 1", child.Generation)
}
}
func TestDelete(t *testing.T) {
db := openTestDB(t)
setupTestSchema(t, db)
s := NewStore(db)
ctx := context.Background()
id, err := s.Create(ctx, &Program{
Code: "to-delete", Language: "go", Island: IslandDelta,
BehaviorVector: []float64{0.5, 0.5}, ParentIDs: []int64{},
})
if err != nil {
t.Fatalf("Create: %v", err)
}
// Verify it exists
got, _ := s.Get(ctx, id)
if got == nil {
t.Fatal("program should exist before deletion")
}
// Delete it
if err := s.Delete(ctx, id); err != nil {
t.Fatalf("Delete: %v", err)
}
// Verify it's gone
got, _ = s.Get(ctx, id)
if got != nil {
t.Error("program should not exist after deletion")
}
}
func TestList(t *testing.T) {
db := openTestDB(t)
setupTestSchema(t, db)
s := NewStore(db)
ctx := context.Background()
// Create 3 programs
for i := 0; i < 3; i++ {
if _, err := s.Create(ctx, &Program{
Code: string(rune('a' + i)),
Language: "go",
Island: IslandAlpha,
BehaviorVector: []float64{0.5, 0.5},
ParentIDs: []int64{},
}); err != nil {
t.Fatalf("Create %d: %v", i, err)
}
}
// List with limit 2
list, err := s.List(ctx, 2, 0)
if err != nil {
t.Fatalf("List: %v", err)
}
if len(list) != 2 {
t.Errorf("expected 2 programs with limit=2, got %d", len(list))
}
// List with offset
list2, err := s.List(ctx, 2, 2)
if err != nil {
t.Fatalf("List with offset: %v", err)
}
if len(list2) != 1 {
t.Errorf("expected 1 program with limit=2 offset=2, got %d", len(list2))
}
}
func TestListTopByIsland(t *testing.T) {
db := openTestDB(t)
setupTestSchema(t, db)
s := NewStore(db)
ctx := context.Background()
// Create programs with different fitness values
for _, p := range []*Program{
{Code: "a", Language: "go", Island: IslandAlpha, BehaviorVector: []float64{0.9, 0.1}, Fitness: 100.0, ParentIDs: []int64{}},
{Code: "b", Language: "go", Island: IslandAlpha, BehaviorVector: []float64{0.8, 0.2}, Fitness: 50.0, ParentIDs: []int64{}},
{Code: "c", Language: "go", Island: IslandAlpha, BehaviorVector: []float64{0.7, 0.3}, Fitness: 75.0, ParentIDs: []int64{}},
} {
if _, err := s.Create(ctx, p); err != nil {
t.Fatalf("Create: %v", err)
}
}
top, err := s.ListTopByIsland(ctx, IslandAlpha, 2)
if err != nil {
t.Fatalf("ListTopByIsland: %v", err)
}
if len(top) != 2 {
t.Fatalf("expected 2 top programs, got %d", len(top))
}
// Should be ordered by fitness DESC
if top[0].Fitness != 100.0 || top[1].Fitness != 75.0 {
t.Errorf("expected fitness order [100, 75], got [%f, %f]", top[0].Fitness, top[1].Fitness)
}
}
func TestGetLineage(t *testing.T) {
db := openTestDB(t)
setupTestSchema(t, db)
s := NewStore(db)
ctx := context.Background()
// Create grandparent -> parent -> child lineage
grandparent, _ := s.Create(ctx, &Program{
Code: "grandparent", Language: "go", Island: IslandAlpha,
BehaviorVector: []float64{0.9, 0.1}, ParentIDs: []int64{},
})
parent, _ := s.Create(ctx, &Program{
Code: "parent", Language: "go", Island: IslandAlpha,
BehaviorVector: []float64{0.8, 0.2}, ParentIDs: []int64{grandparent},
})
child, _ := s.Create(ctx, &Program{
Code: "child", Language: "go", Island: IslandAlpha,
BehaviorVector: []float64{0.7, 0.3}, ParentIDs: []int64{parent},
})
lineage, err := s.GetLineage(ctx, child)
if err != nil {
t.Fatalf("GetLineage: %v", err)
}
if len(lineage) != 3 {
t.Errorf("expected 3 ancestors in lineage, got %d: %v", len(lineage), lineage)
}
}