1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
package storage
import (
"database/sql"
"time"
"github.com/thepeterstone/claudomator/internal/story"
)
// CreateEpic inserts a new epic. Defaults Status to "OPEN" if unset.
func (s *DB) CreateEpic(e *story.Epic) error {
if e.Status == "" {
e.Status = "OPEN"
}
now := time.Now().UTC()
e.CreatedAt = now
e.UpdatedAt = now
_, err := s.db.Exec(
`INSERT INTO epics (id, name, description, status, discovery_source, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
e.ID, e.Name, e.Description, e.Status, e.DiscoverySource, now, now,
)
return err
}
// GetEpic retrieves an epic by ID.
func (s *DB) GetEpic(id string) (*story.Epic, error) {
row := s.db.QueryRow(`SELECT id, name, description, status, discovery_source, created_at, updated_at FROM epics WHERE id = ?`, id)
return scanEpic(row)
}
// GetEpicByName retrieves an epic by exact name match (the earliest-created
// one, if more than one somehow shares a name), or sql.ErrNoRows if none
// exists. Used by internal/executor's ProposeEpic (Phase 7c) to decide
// whether a discovery/planner agent's proposed epic name refers to an
// existing epic or needs a new one — simplest reasonable matching, no fuzzy
// dedup.
func (s *DB) GetEpicByName(name string) (*story.Epic, error) {
row := s.db.QueryRow(`SELECT id, name, description, status, discovery_source, created_at, updated_at FROM epics WHERE name = ? ORDER BY created_at ASC LIMIT 1`, name)
return scanEpic(row)
}
// ListEpics returns epics, optionally filtered by status. Pass an empty
// string to list all epics.
func (s *DB) ListEpics(status string) ([]*story.Epic, error) {
query := `SELECT id, name, description, status, discovery_source, created_at, updated_at FROM epics WHERE 1=1`
var args []interface{}
if status != "" {
query += " AND status = ?"
args = append(args, status)
}
query += " ORDER BY created_at DESC"
rows, err := s.db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var epics []*story.Epic
for rows.Next() {
e, err := scanEpicRows(rows)
if err != nil {
return nil, err
}
epics = append(epics, e)
}
return epics, rows.Err()
}
// UpdateEpic updates an existing epic's name/description/status/discovery_source.
// Mirrors UpdateProject's shape: no existence check, no status-machine validation.
func (s *DB) UpdateEpic(e *story.Epic) error {
now := time.Now().UTC()
_, err := s.db.Exec(
`UPDATE epics SET name = ?, description = ?, status = ?, discovery_source = ?, updated_at = ? WHERE id = ?`,
e.Name, e.Description, e.Status, e.DiscoverySource, now, e.ID,
)
if err != nil {
return err
}
e.UpdatedAt = now
return nil
}
func scanEpic(row scanner) (*story.Epic, error) {
var e story.Epic
var description sql.NullString
var discoverySource sql.NullString
if err := row.Scan(&e.ID, &e.Name, &description, &e.Status, &discoverySource, &e.CreatedAt, &e.UpdatedAt); err != nil {
return nil, err
}
e.Description = description.String
e.DiscoverySource = discoverySource.String
return &e, nil
}
func scanEpicRows(rows *sql.Rows) (*story.Epic, error) {
return scanEpic(rows)
}
|