feat: add subject validation and length checking

Amolith and Crush created

Add buildAndValidateSubject function to construct commit subjects in
conventional commit format and validate they don't exceed 50 characters.
Truncated subjects show exceeding portion with ellipsis in error output.

Implements: bug-5b35298
Co-authored-by: Crush <crush@charm.land>

Change summary

main.go | 43 ++++++++++++++++++++++++++++++++++++++-----
1 file changed, 38 insertions(+), 5 deletions(-)

Detailed changes

main.go 🔗

@@ -6,8 +6,10 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"runtime/debug"
+	"strings"
 
 	"github.com/charmbracelet/fang"
 	"github.com/spf13/cobra"
@@ -47,11 +49,12 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
   -b "Had to do a weird thing because..."
 `,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		// TODO: Implement commit formatting logic here
-		// 1. Validate subject length (type(scope): message <= 50 chars)
-		// 2. Format body as Markdown wrapped to 72 columns
-		// 3. Validate and format trailers
-		// 4. Pipe result to `git commit -F -`
+		subject, err := buildAndValidateSubject(commitType, scope, message, breakingChange)
+		if err != nil {
+			return err
+		}
+
+		_ = subject
 
 		return nil
 	},
@@ -73,6 +76,36 @@ func init() {
 	}
 }
 
+func buildAndValidateSubject(commitType, scope, message string, breaking bool) (string, error) {
+	var subject strings.Builder
+
+	subject.WriteString(commitType)
+
+	if scope != "" {
+		subject.WriteString("(")
+		subject.WriteString(scope)
+		subject.WriteString(")")
+	}
+
+	if breaking {
+		subject.WriteString("!")
+	}
+
+	subject.WriteString(": ")
+	subject.WriteString(message)
+
+	result := subject.String()
+	length := len(result)
+
+	if length > 50 {
+		exceededBy := length - 50
+		truncated := result[:50] + "…"
+		return "", fmt.Errorf("subject exceeds 50 character limit by %d:\n%s", exceededBy, truncated)
+	}
+
+	return result, nil
+}
+
 func main() {
 	ctx := context.Background()