[object Object]

← back to Cli Printing Press

feat(pipeline): copy spec into output dir after generation

a00ebdb2e41256807f15440b6eeab46af9f79ece · 2026-03-27 21:41:24 -0700 · Trevin Chow

Adds copySpecToOutput helper that saves the source spec as
<output-dir>/spec.json when specFlag is "--spec". This enables
standalone scoring without needing the original spec path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a00ebdb2e41256807f15440b6eeab46af9f79ece
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri Mar 27 21:41:24 2026 -0700

    feat(pipeline): copy spec into output dir after generation
    
    Adds copySpecToOutput helper that saves the source spec as
    <output-dir>/spec.json when specFlag is "--spec". This enables
    standalone scoring without needing the original spec path.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/pipeline/fullrun.go      | 23 ++++++++++++++
 internal/pipeline/fullrun_test.go | 66 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+)

diff --git a/internal/pipeline/fullrun.go b/internal/pipeline/fullrun.go
index 749a2c0c..77204e96 100644
--- a/internal/pipeline/fullrun.go
+++ b/internal/pipeline/fullrun.go
@@ -106,6 +106,11 @@ func MakeBestCLI(apiName, level, specFlag, specURL, outputDir, pressBinary strin
 		return result
 	}
 
+	// Step 2.1: Copy spec into output dir for standalone scoring
+	if err := copySpecToOutput(specFlag, specURL, outputDir); err != nil {
+		result.Errors = append(result.Errors, fmt.Sprintf("spec copy: %v", err))
+	}
+
 	// Step 2.5: LLM Polish
 	polishResult, polishErr := llmpolish.Polish(llmpolish.PolishRequest{
 		APIName:   apiName,
@@ -417,6 +422,24 @@ func PrintComparisonTable(results []*FullRunResult) string {
 	return b.String()
 }
 
+// copySpecToOutput copies the source spec file into <outputDir>/spec.json
+// so that standalone scoring can find it without the original spec path.
+// Only copies when specFlag is "--spec" (local file). Errors are non-fatal.
+func copySpecToOutput(specFlag, specURL, outputDir string) error {
+	if specFlag != "--spec" {
+		return nil
+	}
+	data, err := os.ReadFile(specURL)
+	if err != nil {
+		return fmt.Errorf("reading spec %s: %w", specURL, err)
+	}
+	dst := filepath.Join(outputDir, "spec.json")
+	if err := os.WriteFile(dst, data, 0o644); err != nil {
+		return fmt.Errorf("writing %s: %w", dst, err)
+	}
+	return nil
+}
+
 func writeRow(b *strings.Builder, label string, results []*FullRunResult, fn func(*FullRunResult) string) {
 	b.WriteString(fmt.Sprintf("%-25s", label))
 	for _, r := range results {
diff --git a/internal/pipeline/fullrun_test.go b/internal/pipeline/fullrun_test.go
index 997433bb..5a8608db 100644
--- a/internal/pipeline/fullrun_test.go
+++ b/internal/pipeline/fullrun_test.go
@@ -62,6 +62,72 @@ func TestFullRun(t *testing.T) {
 	fmt.Printf("Full results at: %s\n", baseDir)
 }
 
+func TestCopySpecToOutput(t *testing.T) {
+	tests := []struct {
+		name      string
+		specFlag  string
+		setup     func(t *testing.T, dir string) string // returns specURL
+		wantCopy  bool
+		wantError bool
+	}{
+		{
+			name:     "copies spec when flag is --spec",
+			specFlag: "--spec",
+			setup: func(t *testing.T, dir string) string {
+				specPath := filepath.Join(dir, "input-spec.json")
+				require.NoError(t, os.WriteFile(specPath, []byte(`{"openapi":"3.0.0"}`), 0o644))
+				return specPath
+			},
+			wantCopy: true,
+		},
+		{
+			name:     "skips when flag is --docs",
+			specFlag: "--docs",
+			setup: func(t *testing.T, dir string) string {
+				return "https://developers.notion.com/reference"
+			},
+			wantCopy: false,
+		},
+		{
+			name:     "returns error when spec file missing",
+			specFlag: "--spec",
+			setup: func(t *testing.T, dir string) string {
+				return filepath.Join(dir, "nonexistent.json")
+			},
+			wantCopy:  false,
+			wantError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			dir := t.TempDir()
+			outputDir := filepath.Join(dir, "output")
+			require.NoError(t, os.MkdirAll(outputDir, 0o755))
+
+			specURL := tt.setup(t, dir)
+			err := copySpecToOutput(tt.specFlag, specURL, outputDir)
+
+			if tt.wantError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+
+			dst := filepath.Join(outputDir, "spec.json")
+			if tt.wantCopy {
+				data, readErr := os.ReadFile(dst)
+				require.NoError(t, readErr, "spec.json should exist in output dir")
+				expected, _ := os.ReadFile(specURL)
+				assert.Equal(t, expected, data, "spec.json content should match source")
+			} else if !tt.wantError {
+				_, readErr := os.ReadFile(dst)
+				assert.True(t, os.IsNotExist(readErr), "spec.json should not exist")
+			}
+		})
+	}
+}
+
 func findRepoRoot() string {
 	dir, _ := os.Getwd()
 	for {

← 1bebcb0a fix(scorecard): improve accuracy for non-trivial CLIs  ·  back to Cli Printing Press  ·  feat(skill): add /printing-press-score for standalone CLI sc c08aedc7 →