8d98faccd9
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>
266 lines
5.5 KiB
Go
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 := ®istry.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, ", ")
|
|
}
|