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:
@@ -0,0 +1,265 @@
|
||||
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, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user