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:
parent
984ecc1da7
commit
3d9326d767
2 changed files with 236 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue