Files
egutierrez 8d98faccd9 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>
2026-03-28 17:13:24 +01:00

266 lines
5.5 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"text/tabwriter"
"time"
"fn-registry/registry"
)
func cmdProposal(args []string) {
if len(args) < 1 {
printProposalUsage()
os.Exit(1)
}
switch args[0] {
case "add":
cmdProposalAdd(args[1:])
case "list":
cmdProposalList(args[1:])
case "show":
cmdProposalShow(args[1:])
case "update":
cmdProposalUpdate(args[1:])
case "help", "-h", "--help":
printProposalUsage()
default:
fmt.Fprintf(os.Stderr, "unknown proposal command: %s\n", args[0])
printProposalUsage()
os.Exit(1)
}
}
func printProposalUsage() {
fmt.Println(`fn proposal — gestiona proposals
Usage:
fn proposal add --kind <kind> --title <title> [options]
fn proposal list [-k kind] [-s status]
fn proposal show <id>
fn proposal update <id> --status <status> [--reviewed-by <who>]
Kinds: new_function, new_type, improve_function, improve_type, new_pipeline
Status: pending, approved, rejected, implemented`)
}
func cmdProposalAdd(args []string) {
var id, kind, targetID, title, description, evidenceStr, createdBy string
i := 0
for i < len(args) {
switch args[i] {
case "--id":
i++
id = args[i]
case "--kind":
i++
kind = args[i]
case "--target-id":
i++
targetID = args[i]
case "--title":
i++
title = args[i]
case "--description":
i++
description = args[i]
case "--evidence":
i++
evidenceStr = args[i]
case "--created-by":
i++
createdBy = args[i]
}
i++
}
if kind == "" || title == "" {
fmt.Fprintln(os.Stderr, "error: --kind and --title are required")
os.Exit(1)
}
if id == "" {
id = fmt.Sprintf("proposal_%d", time.Now().UnixNano())
}
var evidence map[string]any
if evidenceStr != "" {
if err := json.Unmarshal([]byte(evidenceStr), &evidence); err != nil {
fmt.Fprintf(os.Stderr, "error: invalid evidence JSON: %v\n", err)
os.Exit(1)
}
}
p := &registry.Proposal{
ID: id,
Kind: registry.ProposalKind(kind),
TargetID: targetID,
Title: title,
Description: description,
Evidence: evidence,
Status: registry.ProposalPending,
CreatedBy: createdBy,
}
if err := registry.ValidateProposal(p); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
db := openDB()
defer db.Close()
if err := db.InsertProposal(p); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Created proposal: %s\n", p.ID)
}
func cmdProposalList(args []string) {
var kind, status string
i := 0
for i < len(args) {
switch args[i] {
case "-k":
i++
kind = args[i]
case "-s":
i++
status = args[i]
}
i++
}
db := openDB()
defer db.Close()
proposals, err := db.ListProposals(registry.ProposalKind(kind), registry.ProposalStatus(status))
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if len(proposals) == 0 {
fmt.Println("No proposals.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tKIND\tSTATUS\tTITLE\tCREATED_BY")
for _, p := range proposals {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", p.ID, p.Kind, p.Status, truncate(p.Title, 40), p.CreatedBy)
}
w.Flush()
}
func cmdProposalShow(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: fn proposal show <id>")
os.Exit(1)
}
db := openDB()
defer db.Close()
p, err := db.GetProposal(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("ID: %s\n", p.ID)
fmt.Printf("Kind: %s\n", p.Kind)
fmt.Printf("Status: %s\n", p.Status)
fmt.Printf("Title: %s\n", p.Title)
fmt.Printf("Description: %s\n", p.Description)
if p.TargetID != "" {
fmt.Printf("Target ID: %s\n", p.TargetID)
}
if len(p.Evidence) > 0 {
ev, _ := json.MarshalIndent(p.Evidence, " ", " ")
fmt.Printf("Evidence: %s\n", string(ev))
}
fmt.Printf("Created by: %s\n", p.CreatedBy)
if p.ReviewedBy != "" {
fmt.Printf("Reviewed by: %s\n", p.ReviewedBy)
}
fmt.Printf("Created: %s\n", p.CreatedAt.Format(time.RFC3339))
fmt.Printf("Updated: %s\n", p.UpdatedAt.Format(time.RFC3339))
}
func cmdProposalUpdate(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: fn proposal update <id> --status <status> [--reviewed-by <who>]")
os.Exit(1)
}
id := args[0]
var status, reviewedBy string
i := 1
for i < len(args) {
switch args[i] {
case "--status":
i++
status = args[i]
case "--reviewed-by":
i++
reviewedBy = args[i]
}
i++
}
db := openDB()
defer db.Close()
p, err := db.GetProposal(id)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if status != "" {
p.Status = registry.ProposalStatus(status)
}
if reviewedBy != "" {
p.ReviewedBy = reviewedBy
}
// Validate updated proposal
validKinds := map[string]bool{
"pending": true, "approved": true, "rejected": true, "implemented": true,
}
if status != "" && !validKinds[status] {
fmt.Fprintf(os.Stderr, "error: invalid status %q\n", status)
os.Exit(1)
}
if err := db.UpdateProposal(p); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Updated proposal: %s (status: %s)\n", p.ID, p.Status)
}
func formatEvidence(evidence map[string]any) string {
if len(evidence) == 0 {
return "{}"
}
b, _ := json.MarshalIndent(evidence, "", " ")
return string(b)
}
// formatStrings joins a slice for display, handling nil/empty.
func formatStrings(ss []string) string {
if len(ss) == 0 {
return ""
}
return strings.Join(ss, ", ")
}