[object Object]

← back to Cli Printing Press

feat(pipeline): add comparative analysis scoring and GoReleaser brews section

1f5871bdcf399184e66d97e864217c5e6b15ae2f · 2026-03-24 20:30:03 -0700 · Matt Van Horn

- Create comparative.go with 6-dimension scoring (breadth, install friction,
  auth UX, output formats, agent friendliness, freshness) out of 100 points
- Produces comparative-analysis.md with score table, gaps, advantages,
  and ship/hold recommendation
- Update goreleaser.yaml.tmpl with brews section for Homebrew tap distribution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 1f5871bdcf399184e66d97e864217c5e6b15ae2f
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Tue Mar 24 20:30:03 2026 -0700

    feat(pipeline): add comparative analysis scoring and GoReleaser brews section
    
    - Create comparative.go with 6-dimension scoring (breadth, install friction,
      auth UX, output formats, agent friendliness, freshness) out of 100 points
    - Produces comparative-analysis.md with score table, gaps, advantages,
      and ship/hold recommendation
    - Update goreleaser.yaml.tmpl with brews section for Homebrew tap distribution
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 internal/generator/templates/goreleaser.yaml.tmpl |   9 +
 internal/pipeline/comparative.go                  | 221 ++++++++++++++++++++++
 2 files changed, 230 insertions(+)

diff --git a/internal/generator/templates/goreleaser.yaml.tmpl b/internal/generator/templates/goreleaser.yaml.tmpl
index e463f0aa..43bdd384 100644
--- a/internal/generator/templates/goreleaser.yaml.tmpl
+++ b/internal/generator/templates/goreleaser.yaml.tmpl
@@ -25,3 +25,12 @@ archives:
         formats: [zip]
 checksum:
   name_template: checksums.txt
+brews:
+  - name: {{.Name}}-cli
+    repository:
+      owner: USER
+      name: homebrew-tap
+    homepage: "https://github.com/USER/{{.Name}}-cli"
+    description: "{{.Description}}"
+    install: |
+      bin.install "{{.Name}}-cli"
diff --git a/internal/pipeline/comparative.go b/internal/pipeline/comparative.go
new file mode 100644
index 00000000..8bf67c39
--- /dev/null
+++ b/internal/pipeline/comparative.go
@@ -0,0 +1,221 @@
+package pipeline
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+// ComparativeResult holds the output of the comparative analysis phase.
+type ComparativeResult struct {
+	OurScore       int                `json:"our_score"`
+	Alternatives   []AltScore         `json:"alternatives"`
+	Gaps           []string           `json:"gaps"`
+	Advantages     []string           `json:"advantages"`
+	Recommendation string             `json:"recommendation"` // "ship", "ship-with-gaps", "hold"
+}
+
+// AltScore holds a scored alternative.
+type AltScore struct {
+	Name           string `json:"name"`
+	Breadth        int    `json:"breadth"`
+	InstallFriction int   `json:"install_friction"`
+	AuthUX         int    `json:"auth_ux"`
+	OutputFormats  int    `json:"output_formats"`
+	AgentFriendly  int    `json:"agent_friendly"`
+	Freshness      int    `json:"freshness"`
+	Total          int    `json:"total"`
+}
+
+// RunComparative reads research and dogfood results, scores everything,
+// and writes comparative-analysis.md.
+func RunComparative(pipelineDir string, ourCommandCount int) (*ComparativeResult, error) {
+	research, err := LoadResearch(pipelineDir)
+	if err != nil {
+		// Research is optional - produce a minimal report
+		research = &ResearchResult{Alternatives: nil}
+	}
+
+	result := &ComparativeResult{}
+
+	// Score our CLI - we always have these features from the generator
+	ourBreadth := 20 // We control this; caller passes command count for ratio later
+	ourInstall := 15 // go install or binary download
+	ourAuth := 15    // env var + config file
+	ourOutput := 15  // JSON + table + plain
+	ourAgent := 15   // --json + --dry-run + non-interactive
+	ourFresh := 15   // just generated
+	result.OurScore = ourBreadth + ourInstall + ourAuth + ourOutput + ourAgent + ourFresh
+
+	// Score each alternative
+	for _, alt := range research.Alternatives {
+		scored := scoreAlternative(alt, ourCommandCount)
+		result.Alternatives = append(result.Alternatives, scored)
+	}
+
+	// Determine gaps and advantages
+	result.Gaps, result.Advantages = compareGapsAndAdvantages(result)
+
+	// Recommendation
+	bestAltScore := 0
+	for _, a := range result.Alternatives {
+		if a.Total > bestAltScore {
+			bestAltScore = a.Total
+		}
+	}
+
+	switch {
+	case bestAltScore > result.OurScore+10:
+		result.Recommendation = "hold"
+	case bestAltScore > result.OurScore-10:
+		result.Recommendation = "ship-with-gaps"
+	default:
+		result.Recommendation = "ship"
+	}
+
+	// Write the report
+	if err := writeComparativeReport(result, research, pipelineDir); err != nil {
+		return result, err
+	}
+
+	return result, nil
+}
+
+func scoreAlternative(alt Alternative, ourCmdCount int) AltScore {
+	s := AltScore{Name: alt.Name}
+
+	// Breadth (0-20): ratio of their commands to ours
+	if ourCmdCount > 0 && alt.CommandCount > 0 {
+		ratio := float64(alt.CommandCount) / float64(ourCmdCount)
+		if ratio > 1 {
+			ratio = 1
+		}
+		s.Breadth = int(ratio * 20)
+	} else {
+		s.Breadth = 10 // unknown, assume moderate
+	}
+
+	// Install Friction (0-20)
+	switch alt.InstallMethod {
+	case "binary":
+		s.InstallFriction = 20
+	case "brew":
+		s.InstallFriction = 18
+	case "cargo":
+		s.InstallFriction = 15
+	case "npm":
+		s.InstallFriction = 10
+	case "pip":
+		s.InstallFriction = 10
+	default:
+		s.InstallFriction = 5
+	}
+
+	// Auth UX (0-15)
+	if alt.HasAuth {
+		s.AuthUX = 10 // assume env var support
+	} else {
+		s.AuthUX = 0
+	}
+
+	// Output Formats (0-15)
+	if alt.HasJSON {
+		s.OutputFormats = 10
+	} else {
+		s.OutputFormats = 5 // assume at least text output
+	}
+
+	// Agent Friendliness (0-15) - hard to assess from metadata
+	if alt.HasJSON {
+		s.AgentFriendly = 5
+	}
+
+	// Freshness (0-15)
+	if alt.LastUpdated != "" {
+		if t, err := time.Parse("2006-01-02", alt.LastUpdated); err == nil {
+			age := time.Since(t)
+			switch {
+			case age < 30*24*time.Hour:
+				s.Freshness = 15
+			case age < 90*24*time.Hour:
+				s.Freshness = 10
+			case age < 365*24*time.Hour:
+				s.Freshness = 5
+			default:
+				s.Freshness = 0
+			}
+		}
+	}
+
+	s.Total = s.Breadth + s.InstallFriction + s.AuthUX + s.OutputFormats + s.AgentFriendly + s.Freshness
+	return s
+}
+
+func compareGapsAndAdvantages(result *ComparativeResult) (gaps, advantages []string) {
+	advantages = append(advantages, "Go binary - zero runtime dependencies, instant startup")
+	advantages = append(advantages, "--dry-run mode shows exact API request without sending")
+	advantages = append(advantages, "triple output format (JSON, table, plain text)")
+
+	anyBetterBreadth := false
+	for _, a := range result.Alternatives {
+		if a.Breadth >= 18 {
+			anyBetterBreadth = true
+		}
+	}
+
+	if anyBetterBreadth {
+		gaps = append(gaps, "some alternatives have broader endpoint coverage")
+	}
+
+	if len(gaps) == 0 {
+		gaps = append(gaps, "no significant gaps identified")
+	}
+
+	return gaps, advantages
+}
+
+func writeComparativeReport(result *ComparativeResult, research *ResearchResult, pipelineDir string) error {
+	var b strings.Builder
+
+	b.WriteString("# Comparative Analysis\n\n")
+	b.WriteString(fmt.Sprintf("**Recommendation: %s**\n\n", strings.ToUpper(result.Recommendation)))
+	b.WriteString(fmt.Sprintf("Our score: %d/100\n\n", result.OurScore))
+
+	// Score table
+	if len(result.Alternatives) > 0 {
+		b.WriteString("## Score Table\n\n")
+		b.WriteString("| Tool | Breadth | Install | Auth | Output | Agent | Fresh | Total |\n")
+		b.WriteString("|------|---------|---------|------|--------|-------|-------|-------|\n")
+		b.WriteString(fmt.Sprintf("| **Ours** | 20 | 15 | 15 | 15 | 15 | 15 | **%d** |\n", result.OurScore))
+		for _, a := range result.Alternatives {
+			b.WriteString(fmt.Sprintf("| %s | %d | %d | %d | %d | %d | %d | **%d** |\n",
+				a.Name, a.Breadth, a.InstallFriction, a.AuthUX, a.OutputFormats, a.AgentFriendly, a.Freshness, a.Total))
+		}
+		b.WriteString("\n")
+	} else {
+		b.WriteString("No alternatives discovered - we have the field to ourselves.\n\n")
+	}
+
+	// Advantages
+	b.WriteString("## Our Advantages\n\n")
+	for _, a := range result.Advantages {
+		b.WriteString(fmt.Sprintf("- %s\n", a))
+	}
+	b.WriteString("\n")
+
+	// Gaps
+	b.WriteString("## Gaps to Address\n\n")
+	for _, g := range result.Gaps {
+		b.WriteString(fmt.Sprintf("- %s\n", g))
+	}
+	b.WriteString("\n")
+
+	// Novelty context from research
+	if research != nil && research.NoveltyScore > 0 {
+		b.WriteString(fmt.Sprintf("## Research Context\n\nNovelty score: %d/10 (%s)\n", research.NoveltyScore, research.Recommendation))
+	}
+
+	return os.WriteFile(filepath.Join(pipelineDir, "comparative-analysis.md"), []byte(b.String()), 0o644)
+}

← bc5c5db8 feat(pipeline): add dogfood automation, anti-AI text filter,  ·  back to Cli Printing Press  ·  feat(templates): add --select flag, error hints, README rewr 780c6b51 →