package infra import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "time" _ "github.com/mattn/go-sqlite3" ) // ProposalFromFailure creates a proposal row in registry.db for each failed // CheckResult. It opens the database at registryDB, filters results with // Status=="fail", and inserts one proposal per failure using: // - kind="new_function" for severity=="critical" checks (highest urgency proxy) // - kind="improve_function" for severity=="warning" checks // // Note: the proposals table kind constraint only allows // (new_function, new_type, improve_function, improve_type, new_pipeline). // Until a dedicated "bug" kind is added, we use new_function/improve_function // as the closest proxies for critical and warning failures respectively. // // Returns the list of proposal IDs created, or an error if the DB cannot be // opened or any INSERT fails. func ProposalFromFailure(registryDB string, appID string, results []CheckResult, executionID string) ([]string, error) { db, err := SQLiteOpen(registryDB, "") if err != nil { return nil, fmt.Errorf("proposal_from_failure: open registry db: %w", err) } defer db.Close() var created []string now := time.Now().UTC().Format(time.RFC3339) for _, r := range results { if r.Status != "fail" { continue } propID, err := generatePropID() if err != nil { return created, fmt.Errorf("proposal_from_failure: generate id: %w", err) } kind := proposalKind(r.Severity) title := fmt.Sprintf("e2e fail: %s::%s", appID, r.ID) desc := buildDescription(r) evidence, _ := json.Marshal(map[string]any{ "check_id": r.ID, "execution_id": executionID, "exit_code": r.ExitCode, "error": r.Error, "severity": r.Severity, }) _, err = db.Exec(` INSERT INTO proposals (id, kind, target_id, title, description, evidence, status, created_by, reviewed_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'pending', 'reactive_loop', '', ?, ?)`, propID, kind, appID, title, desc, string(evidence), now, now, ) if err != nil { return created, fmt.Errorf("proposal_from_failure: insert proposal %s: %w", propID, err) } created = append(created, propID) } return created, nil } // proposalKind maps check severity to an allowed proposals.kind value. // critical -> new_function (highest urgency proxy) // warning -> improve_function func proposalKind(severity string) string { if severity == "warning" { return "improve_function" } return "new_function" } // buildDescription assembles a human-readable description for the proposal. func buildDescription(r CheckResult) string { desc := fmt.Sprintf("E2E check %q failed (severity: %s, exit_code: %d).", r.ID, r.Severity, r.ExitCode) if r.Error != "" { desc += "\n\nError: " + r.Error } if r.Stdout != "" { desc += "\n\nStdout:\n" + r.Stdout } if r.Stderr != "" { desc += "\n\nStderr:\n" + r.Stderr } desc += "\n\nSugerencia: revisar el comando/endpoint del check y el estado del servicio." return desc } // generatePropID generates a random proposal ID of the form "prop_<16hexchars>". func generatePropID() (string, error) { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { return "", fmt.Errorf("crypto/rand: %w", err) } return "prop_" + hex.EncodeToString(b), nil }