feat: proposals en registry
Añade sistema de proposals al registry: modelos (ProposalKind, ProposalStatus), CRUD completo (Insert/Get/Update/Delete/List/Search con FTS), validación, migración 002_proposals.sql y subcomando CLI fn proposal (add/list/show/update). Motor de migraciones con embed.FS reemplaza schema estático. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,23 @@ func unmarshalStrings(s string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func marshalJSON(v map[string]any) string {
|
||||
if v == nil {
|
||||
v = map[string]any{}
|
||||
}
|
||||
b, _ := json.Marshal(v)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func unmarshalJSON(s string) map[string]any {
|
||||
var out map[string]any
|
||||
json.Unmarshal([]byte(s), &out)
|
||||
if out == nil {
|
||||
out = map[string]any{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func marshalProps(ps []PropDef) string {
|
||||
if ps == nil {
|
||||
ps = []PropDef{}
|
||||
@@ -305,3 +322,153 @@ func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Proposal CRUD ---
|
||||
|
||||
// InsertProposal inserts or replaces a proposal.
|
||||
func (db *DB) InsertProposal(p *Proposal) error {
|
||||
now := time.Now().UTC()
|
||||
if p.CreatedAt.IsZero() {
|
||||
p.CreatedAt = now
|
||||
}
|
||||
p.UpdatedAt = now
|
||||
|
||||
if p.Status == "" {
|
||||
p.Status = ProposalPending
|
||||
}
|
||||
|
||||
_, err := db.conn.Exec(`
|
||||
INSERT OR REPLACE INTO proposals (
|
||||
id, kind, target_id, title, description, evidence,
|
||||
status, created_by, reviewed_by, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
p.ID, string(p.Kind), p.TargetID, p.Title, p.Description,
|
||||
marshalJSON(p.Evidence), string(p.Status), p.CreatedBy, p.ReviewedBy,
|
||||
p.CreatedAt.Format(time.RFC3339), p.UpdatedAt.Format(time.RFC3339),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProposal returns a proposal by ID.
|
||||
func (db *DB) GetProposal(id string) (*Proposal, error) {
|
||||
rows, err := db.conn.Query(`
|
||||
SELECT id, kind, target_id, title, description, evidence,
|
||||
status, created_by, reviewed_by, created_at, updated_at
|
||||
FROM proposals WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
ps, err := scanProposals(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ps) == 0 {
|
||||
return nil, fmt.Errorf("proposal %q not found", id)
|
||||
}
|
||||
return &ps[0], nil
|
||||
}
|
||||
|
||||
// UpdateProposal updates an existing proposal.
|
||||
func (db *DB) UpdateProposal(p *Proposal) error {
|
||||
p.UpdatedAt = time.Now().UTC()
|
||||
_, err := db.conn.Exec(`
|
||||
UPDATE proposals SET kind=?, target_id=?, title=?, description=?, evidence=?,
|
||||
status=?, created_by=?, reviewed_by=?, updated_at=?
|
||||
WHERE id=?`,
|
||||
string(p.Kind), p.TargetID, p.Title, p.Description,
|
||||
marshalJSON(p.Evidence), string(p.Status), p.CreatedBy, p.ReviewedBy,
|
||||
p.UpdatedAt.Format(time.RFC3339), p.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteProposal removes a proposal by ID.
|
||||
func (db *DB) DeleteProposal(id string) error {
|
||||
_, err := db.conn.Exec("DELETE FROM proposals WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListProposals returns proposals filtered by kind and/or status.
|
||||
func (db *DB) ListProposals(kind ProposalKind, status ProposalStatus) ([]Proposal, error) {
|
||||
where := []string{}
|
||||
args := []any{}
|
||||
if kind != "" {
|
||||
where = append(where, "kind = ?")
|
||||
args = append(args, string(kind))
|
||||
}
|
||||
if status != "" {
|
||||
where = append(where, "status = ?")
|
||||
args = append(args, string(status))
|
||||
}
|
||||
|
||||
q := `SELECT id, kind, target_id, title, description, evidence,
|
||||
status, created_by, reviewed_by, created_at, updated_at
|
||||
FROM proposals`
|
||||
if len(where) > 0 {
|
||||
q += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
q += " ORDER BY created_at DESC"
|
||||
|
||||
rows, err := db.conn.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanProposals(rows)
|
||||
}
|
||||
|
||||
// SearchProposals performs FTS search on proposals with optional filters.
|
||||
func (db *DB) SearchProposals(query string, kind ProposalKind, status ProposalStatus) ([]Proposal, error) {
|
||||
where := []string{}
|
||||
args := []any{}
|
||||
|
||||
if query != "" {
|
||||
where = append(where, "p.id IN (SELECT id FROM proposals_fts WHERE proposals_fts MATCH ?)")
|
||||
args = append(args, query)
|
||||
}
|
||||
if kind != "" {
|
||||
where = append(where, "p.kind = ?")
|
||||
args = append(args, string(kind))
|
||||
}
|
||||
if status != "" {
|
||||
where = append(where, "p.status = ?")
|
||||
args = append(args, string(status))
|
||||
}
|
||||
|
||||
q := `SELECT p.id, p.kind, p.target_id, p.title, p.description, p.evidence,
|
||||
p.status, p.created_by, p.reviewed_by, p.created_at, p.updated_at
|
||||
FROM proposals p`
|
||||
if len(where) > 0 {
|
||||
q += " WHERE " + strings.Join(where, " AND ")
|
||||
}
|
||||
q += " ORDER BY p.created_at DESC"
|
||||
|
||||
rows, err := db.conn.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanProposals(rows)
|
||||
}
|
||||
|
||||
func scanProposals(rows interface{ Next() bool; Scan(...any) error }) ([]Proposal, error) {
|
||||
var result []Proposal
|
||||
for rows.Next() {
|
||||
var p Proposal
|
||||
var evidenceJSON, createdAt, updatedAt string
|
||||
err := rows.Scan(
|
||||
&p.ID, &p.Kind, &p.TargetID, &p.Title, &p.Description, &evidenceJSON,
|
||||
&p.Status, &p.CreatedBy, &p.ReviewedBy, &createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning proposal: %w", err)
|
||||
}
|
||||
p.Evidence = unmarshalJSON(evidenceJSON)
|
||||
p.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
p.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user