[object Object]

← back to Cli Printing Press

fix(cli): scorecard human output shows N/A for opt-out dimensions (#406)

0954bcafcc0142cd0517aa2faf48c2e0ba4ce687 · 2026-04-29 20:55:14 -0700 · Trevin Chow

* fix(cli): scorecard human output shows N/A for opt-out dimensions

The scorecard's human-readable print was missing six dimensions
entirely (mcp_token_efficiency, mcp_remote_transport, mcp_tool_design,
mcp_surface_strategy, cache_freshness, live_api_verification, plus
agent_workflow). Three of those mark themselves "didn't measure" via
sc.UnscoredDimensions when the spec hasn't opted into the feature
they score (no MCP intents, no cache, etc.); the scorer's composite
denominator already excludes them, but the missing per-dimension
print line meant the user only saw "0/10" via downstream consumers
that read scorecard.json directly. Reads as a defect when in fact
the dimension was never scored.

Add the missing dimensions to renderHumanScorecard. The optional ones
go through the existing renderScore (now `render`) closure so they
display as N/A when sc.IsDimensionUnscored returns true. The agent
workflow dimension is required, so it just renders %d/10.

Extract the print into a top-level renderHumanScorecard(io.Writer,
*Scorecard) function so it's directly testable.

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

* refactor(cli): drop stale renderOptionalScore reference in doc comment

The renderHumanScorecard doc comment named "renderOptionalScore" — a
function that doesn't exist; the closure inside the function is just
called "render". One-line fix from the /simplify pass.

Larger structural cleanups the simplify reviewers surfaced — dimension-
name string constants and a table-driven render shared with
writeScorecardMD — touch existing scorecard.go callers and warrant
their own PRs rather than being squeezed into this one.

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

* fix(cli): scorecard prints MCPQuality + extract dimension-name constants

Two review follow-ups:

1. Add the missing MCP Quality print line. The scorecard scores
   mcp_quality (separate from mcp_description_quality), includes it in
   the tier-1 raw sum, and surfaces it in writeScorecardMD's table —
   but the human print path skipped it entirely. Users running
   `printing-press scorecard` saw a Total that didn't account for one
   of the dimensions feeding it. Now prints `MCP Quality %d/10`
   alongside the others.

2. Extract dimension-name string constants in
   internal/pipeline/scorecard.go (DimMCPDescriptionQuality,
   DimMCPTokenEfficiency, ...). The dimension keys are referenced at
   ~10 sites — recordOptionalScore calls, scorecardTierMax calls,
   UnscoredDimensions appends, and writeScorecardMD's data tables —
   plus the new render() call sites in cli/scorecard.go. A typo at
   any one site silently flips the dimension's classification because
   IsDimensionUnscored does a string match against UnscoredDimensions.
   Constants make every typo a compile error.

The constants only cover dimensions that can land in
UnscoredDimensions (the silent-bug class). Always-scored dimensions
remain string literals; they don't go through the unscored-set
machinery and the typo risk doesn't apply.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 0954bcafcc0142cd0517aa2faf48c2e0ba4ce687
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Wed Apr 29 20:55:14 2026 -0700

    fix(cli): scorecard human output shows N/A for opt-out dimensions (#406)
    
    * fix(cli): scorecard human output shows N/A for opt-out dimensions
    
    The scorecard's human-readable print was missing six dimensions
    entirely (mcp_token_efficiency, mcp_remote_transport, mcp_tool_design,
    mcp_surface_strategy, cache_freshness, live_api_verification, plus
    agent_workflow). Three of those mark themselves "didn't measure" via
    sc.UnscoredDimensions when the spec hasn't opted into the feature
    they score (no MCP intents, no cache, etc.); the scorer's composite
    denominator already excludes them, but the missing per-dimension
    print line meant the user only saw "0/10" via downstream consumers
    that read scorecard.json directly. Reads as a defect when in fact
    the dimension was never scored.
    
    Add the missing dimensions to renderHumanScorecard. The optional ones
    go through the existing renderScore (now `render`) closure so they
    display as N/A when sc.IsDimensionUnscored returns true. The agent
    workflow dimension is required, so it just renders %d/10.
    
    Extract the print into a top-level renderHumanScorecard(io.Writer,
    *Scorecard) function so it's directly testable.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * refactor(cli): drop stale renderOptionalScore reference in doc comment
    
    The renderHumanScorecard doc comment named "renderOptionalScore" — a
    function that doesn't exist; the closure inside the function is just
    called "render". One-line fix from the /simplify pass.
    
    Larger structural cleanups the simplify reviewers surfaced — dimension-
    name string constants and a table-driven render shared with
    writeScorecardMD — touch existing scorecard.go callers and warrant
    their own PRs rather than being squeezed into this one.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * fix(cli): scorecard prints MCPQuality + extract dimension-name constants
    
    Two review follow-ups:
    
    1. Add the missing MCP Quality print line. The scorecard scores
       mcp_quality (separate from mcp_description_quality), includes it in
       the tier-1 raw sum, and surfaces it in writeScorecardMD's table —
       but the human print path skipped it entirely. Users running
       `printing-press scorecard` saw a Total that didn't account for one
       of the dimensions feeding it. Now prints `MCP Quality %d/10`
       alongside the others.
    
    2. Extract dimension-name string constants in
       internal/pipeline/scorecard.go (DimMCPDescriptionQuality,
       DimMCPTokenEfficiency, ...). The dimension keys are referenced at
       ~10 sites — recordOptionalScore calls, scorecardTierMax calls,
       UnscoredDimensions appends, and writeScorecardMD's data tables —
       plus the new render() call sites in cli/scorecard.go. A typo at
       any one site silently flips the dimension's classification because
       IsDimensionUnscored does a string match against UnscoredDimensions.
       Constants make every typo a compile error.
    
    The constants only cover dimensions that can land in
    UnscoredDimensions (the silent-bug class). Always-scored dimensions
    remain string literals; they don't go through the unscored-set
    machinery and the typo risk doesn't apply.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/cli/scorecard.go      |  84 ++++++++++++++++++-------------
 internal/cli/scorecard_test.go | 110 +++++++++++++++++++++++++++++++++++++++++
 internal/pipeline/scorecard.go |  63 ++++++++++++++---------
 3 files changed, 202 insertions(+), 55 deletions(-)

diff --git a/internal/cli/scorecard.go b/internal/cli/scorecard.go
index 29444c40..4d0d99cd 100644
--- a/internal/cli/scorecard.go
+++ b/internal/cli/scorecard.go
@@ -3,6 +3,7 @@ package cli
 import (
 	"encoding/json"
 	"fmt"
+	"io"
 	"os"
 	"strings"
 	"time"
@@ -74,39 +75,7 @@ func newScorecardCmd() *cobra.Command {
 			}
 
 			// Human-readable output
-			s := sc.Steinberger
-			renderScore := func(name string, score, max int) string {
-				if sc.IsDimensionUnscored(name) {
-					return "N/A"
-				}
-				return fmt.Sprintf("%d/%d", score, max)
-			}
-
-			fmt.Printf("Quality Scorecard: %s\n\n", sc.APIName)
-			fmt.Printf("  Output Modes   %d/10\n", s.OutputModes)
-			fmt.Printf("  Auth           %d/10\n", s.Auth)
-			fmt.Printf("  Error Handling %d/10\n", s.ErrorHandling)
-			fmt.Printf("  Terminal UX    %d/10\n", s.TerminalUX)
-			fmt.Printf("  README         %d/10\n", s.README)
-			fmt.Printf("  Doctor         %d/10\n", s.Doctor)
-			fmt.Printf("  Agent Native   %d/10\n", s.AgentNative)
-			fmt.Printf("  MCP Desc Quality %s\n", renderScore("mcp_description_quality", s.MCPDescriptionQuality, 10))
-			fmt.Printf("  Local Cache    %d/10\n", s.LocalCache)
-			fmt.Printf("  Breadth        %d/10\n", s.Breadth)
-			fmt.Printf("  Vision         %d/10\n", s.Vision)
-			fmt.Printf("  Workflows      %d/10\n", s.Workflows)
-			fmt.Printf("  Insight        %d/10\n", s.Insight)
-			fmt.Printf("\n  Domain Correctness\n")
-			fmt.Printf("  Path Validity          %s\n", renderScore("path_validity", s.PathValidity, 10))
-			fmt.Printf("  Auth Protocol          %s\n", renderScore("auth_protocol", s.AuthProtocol, 10))
-			fmt.Printf("  Data Pipeline Integrity %d/10\n", s.DataPipelineIntegrity)
-			fmt.Printf("  Sync Correctness       %d/10\n", s.SyncCorrectness)
-			fmt.Printf("  Type Fidelity          %d/5\n", s.TypeFidelity)
-			fmt.Printf("  Dead Code              %d/5\n", s.DeadCode)
-			fmt.Printf("\n  Total: %d/100 - Grade %s\n", s.Total, sc.OverallGrade)
-			if len(sc.UnscoredDimensions) > 0 {
-				fmt.Printf("  Note: omitted from denominator: %s\n", strings.Join(sc.UnscoredDimensions, ", "))
-			}
+			renderHumanScorecard(os.Stdout, sc)
 
 			if live != nil {
 				fmt.Printf("\nLive Check (behavioral sample)\n")
@@ -161,3 +130,52 @@ func newScorecardCmd() *cobra.Command {
 
 	return cmd
 }
+
+// renderHumanScorecard writes the human-readable scorecard table to w.
+// Optional dimensions (those that may land in sc.UnscoredDimensions
+// when the spec hasn't opted into the feature being measured) render
+// as "N/A" so the scorer's "didn't measure" signal isn't flattened
+// into a 0/10 that reads as a defect.
+func renderHumanScorecard(w io.Writer, sc *pipeline.Scorecard) {
+	s := sc.Steinberger
+	render := func(name string, score, max int) string {
+		if sc.IsDimensionUnscored(name) {
+			return "N/A"
+		}
+		return fmt.Sprintf("%d/%d", score, max)
+	}
+
+	fmt.Fprintf(w, "Quality Scorecard: %s\n\n", sc.APIName)
+	fmt.Fprintf(w, "  Output Modes         %d/10\n", s.OutputModes)
+	fmt.Fprintf(w, "  Auth                 %d/10\n", s.Auth)
+	fmt.Fprintf(w, "  Error Handling       %d/10\n", s.ErrorHandling)
+	fmt.Fprintf(w, "  Terminal UX          %d/10\n", s.TerminalUX)
+	fmt.Fprintf(w, "  README               %d/10\n", s.README)
+	fmt.Fprintf(w, "  Doctor               %d/10\n", s.Doctor)
+	fmt.Fprintf(w, "  Agent Native         %d/10\n", s.AgentNative)
+	fmt.Fprintf(w, "  MCP Quality          %d/10\n", s.MCPQuality)
+	fmt.Fprintf(w, "  MCP Desc Quality     %s\n", render(pipeline.DimMCPDescriptionQuality, s.MCPDescriptionQuality, 10))
+	fmt.Fprintf(w, "  MCP Token Efficiency %s\n", render(pipeline.DimMCPTokenEfficiency, s.MCPTokenEff, 10))
+	fmt.Fprintf(w, "  MCP Remote Transport %s\n", render(pipeline.DimMCPRemoteTransport, s.MCPRemoteTransport, 10))
+	fmt.Fprintf(w, "  MCP Tool Design      %s\n", render(pipeline.DimMCPToolDesign, s.MCPToolDesign, 10))
+	fmt.Fprintf(w, "  MCP Surface Strategy %s\n", render(pipeline.DimMCPSurfaceStrategy, s.MCPSurfaceStrategy, 10))
+	fmt.Fprintf(w, "  Local Cache          %d/10\n", s.LocalCache)
+	fmt.Fprintf(w, "  Cache Freshness      %s\n", render(pipeline.DimCacheFreshness, s.CacheFreshness, 10))
+	fmt.Fprintf(w, "  Breadth              %d/10\n", s.Breadth)
+	fmt.Fprintf(w, "  Vision               %d/10\n", s.Vision)
+	fmt.Fprintf(w, "  Workflows            %d/10\n", s.Workflows)
+	fmt.Fprintf(w, "  Insight              %d/10\n", s.Insight)
+	fmt.Fprintf(w, "  Agent Workflow       %d/10\n", s.AgentWorkflow)
+	fmt.Fprintf(w, "\n  Domain Correctness\n")
+	fmt.Fprintf(w, "  Path Validity           %s\n", render(pipeline.DimPathValidity, s.PathValidity, 10))
+	fmt.Fprintf(w, "  Auth Protocol           %s\n", render(pipeline.DimAuthProtocol, s.AuthProtocol, 10))
+	fmt.Fprintf(w, "  Data Pipeline Integrity %d/10\n", s.DataPipelineIntegrity)
+	fmt.Fprintf(w, "  Sync Correctness        %d/10\n", s.SyncCorrectness)
+	fmt.Fprintf(w, "  Live API Verification   %s\n", render(pipeline.DimLiveAPIVerification, s.LiveAPIVerification, 10))
+	fmt.Fprintf(w, "  Type Fidelity           %d/5\n", s.TypeFidelity)
+	fmt.Fprintf(w, "  Dead Code               %d/5\n", s.DeadCode)
+	fmt.Fprintf(w, "\n  Total: %d/100 - Grade %s\n", s.Total, sc.OverallGrade)
+	if len(sc.UnscoredDimensions) > 0 {
+		fmt.Fprintf(w, "  Note: omitted from denominator: %s\n", strings.Join(sc.UnscoredDimensions, ", "))
+	}
+}
diff --git a/internal/cli/scorecard_test.go b/internal/cli/scorecard_test.go
new file mode 100644
index 00000000..d685897b
--- /dev/null
+++ b/internal/cli/scorecard_test.go
@@ -0,0 +1,110 @@
+package cli
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"github.com/mvanhorn/cli-printing-press/v2/internal/pipeline"
+	"github.com/stretchr/testify/assert"
+)
+
+// TestRenderHumanScorecardShowsNAForUnscoredDimensions — the scorer
+// reports "didn't measure this" via the UnscoredDimensions slice.
+// Without N/A rendering the user sees "MCP Tool Design 0/10" alongside
+// real defects and reads it as a real defect, when in fact the spec
+// hasn't opted into the feature being scored.
+func TestRenderHumanScorecardShowsNAForUnscoredDimensions(t *testing.T) {
+	t.Parallel()
+	sc := &pipeline.Scorecard{
+		APIName:      "test-api",
+		OverallGrade: "B",
+		Steinberger: pipeline.SteinerScore{
+			OutputModes:           10,
+			Auth:                  10,
+			ErrorHandling:         10,
+			TerminalUX:            9,
+			README:                8,
+			Doctor:                10,
+			AgentNative:           10,
+			MCPDescriptionQuality: 10,
+			LocalCache:            10,
+			Breadth:               7,
+			Vision:                8,
+			Workflows:             8,
+			Insight:               6,
+			AgentWorkflow:         9,
+			Total:                 85,
+		},
+		UnscoredDimensions: []string{
+			pipeline.DimMCPTokenEfficiency,
+			pipeline.DimMCPRemoteTransport,
+			pipeline.DimMCPToolDesign,
+			pipeline.DimMCPSurfaceStrategy,
+			pipeline.DimCacheFreshness,
+			pipeline.DimAuthProtocol,
+			pipeline.DimLiveAPIVerification,
+		},
+	}
+
+	var buf bytes.Buffer
+	renderHumanScorecard(&buf, sc)
+	got := buf.String()
+
+	for _, dim := range []string{
+		"MCP Token Efficiency N/A",
+		"MCP Remote Transport N/A",
+		"MCP Tool Design      N/A",
+		"MCP Surface Strategy N/A",
+		"Cache Freshness      N/A",
+		"Auth Protocol           N/A",
+		"Live API Verification   N/A",
+	} {
+		assert.Contains(t, got, dim, "expected %q in human output", dim)
+	}
+
+	assert.NotContains(t, got, "MCP Tool Design      0/10",
+		"unscored dimensions must not render as 0/10")
+	assert.NotContains(t, got, "Cache Freshness      0/10",
+		"unscored cache_freshness must not render as 0/10")
+
+	// Note line lists every unscored dimension so the user knows the
+	// composite denominator is reduced.
+	for _, dim := range sc.UnscoredDimensions {
+		assert.Contains(t, got, dim,
+			"unscored-dimensions note must list %q", dim)
+	}
+	assert.Contains(t, got, "omitted from denominator")
+}
+
+// TestRenderHumanScorecardShowsScoreForScoredDimensions — when a
+// dimension IS scored (even at 0/10), the score renders as-is. A
+// genuine 0/10 — e.g., cache_freshness on a CLI that has a local
+// store but hasn't declared the freshness contract — is a real
+// finding the user should see, not be masked.
+func TestRenderHumanScorecardShowsScoreForScoredDimensions(t *testing.T) {
+	t.Parallel()
+	sc := &pipeline.Scorecard{
+		APIName: "test-api",
+		Steinberger: pipeline.SteinerScore{
+			OutputModes:    10,
+			LocalCache:     10,
+			CacheFreshness: 0, // scored, but at 0 — a real finding
+		},
+		// CacheFreshness is NOT in UnscoredDimensions: the scorer
+		// found a local store and assessed the freshness contract
+		// (it's just absent).
+		UnscoredDimensions: nil,
+	}
+
+	var buf bytes.Buffer
+	renderHumanScorecard(&buf, sc)
+	got := buf.String()
+
+	assert.Contains(t, got, "Cache Freshness      0/10",
+		"scored cache_freshness=0 must render as 0/10, not N/A")
+	assert.NotContains(t, got, "Cache Freshness      N/A",
+		"scored dimension at 0 is not the same as unscored")
+	assert.False(t, strings.Contains(got, "omitted from denominator"),
+		"with no unscored dimensions, the composite-note line must not appear")
+}
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index 32c2427f..c4ac428d 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -76,6 +76,25 @@ type SteinerScore struct {
 	CalibrationNote       string `json:"calibration_note,omitempty"`
 }
 
+// Dimension identifiers used by recordOptionalScore, scorecardTierMax,
+// IsDimensionUnscored, and renderers (renderHumanScorecard,
+// writeScorecardMD). Any dimension that can land in
+// Scorecard.UnscoredDimensions has a constant here so a typo at any
+// call site fails the compile rather than silently returning false
+// from IsDimensionUnscored. The string values match the JSON struct
+// tags above and must stay in lockstep.
+const (
+	DimMCPDescriptionQuality = "mcp_description_quality"
+	DimMCPTokenEfficiency    = "mcp_token_efficiency"
+	DimMCPRemoteTransport    = "mcp_remote_transport"
+	DimMCPToolDesign         = "mcp_tool_design"
+	DimMCPSurfaceStrategy    = "mcp_surface_strategy"
+	DimCacheFreshness        = "cache_freshness"
+	DimPathValidity          = "path_validity"
+	DimAuthProtocol          = "auth_protocol"
+	DimLiveAPIVerification   = "live_api_verification"
+)
+
 // CompScore compares our score against a competitor on a single dimension.
 type CompScore struct {
 	Name       string `json:"name"`
@@ -120,18 +139,18 @@ func scoreInfrastructureDimensions(sc *Scorecard, outputDir string) {
 	sc.Steinberger.AgentNative = scoreAgentNative(outputDir)
 	sc.Steinberger.MCPQuality = scoreMCPQuality(outputDir)
 	mcpDescScore, mcpDescScored := scoreMCPDescriptionQuality(outputDir)
-	recordOptionalScore(sc, &sc.Steinberger.MCPDescriptionQuality, "mcp_description_quality", mcpDescScore, mcpDescScored)
+	recordOptionalScore(sc, &sc.Steinberger.MCPDescriptionQuality, DimMCPDescriptionQuality, mcpDescScore, mcpDescScored)
 	mcpTokenScore, mcpTokenScored := scoreMCPTokenEfficiency(outputDir)
-	recordOptionalScore(sc, &sc.Steinberger.MCPTokenEff, "mcp_token_efficiency", mcpTokenScore, mcpTokenScored)
+	recordOptionalScore(sc, &sc.Steinberger.MCPTokenEff, DimMCPTokenEfficiency, mcpTokenScore, mcpTokenScored)
 	remoteScore, remoteScored := scoreMCPRemoteTransport(outputDir)
-	recordOptionalScore(sc, &sc.Steinberger.MCPRemoteTransport, "mcp_remote_transport", remoteScore, remoteScored)
+	recordOptionalScore(sc, &sc.Steinberger.MCPRemoteTransport, DimMCPRemoteTransport, remoteScore, remoteScored)
 	toolDesignScore, toolDesignScored := scoreMCPToolDesign(outputDir)
-	recordOptionalScore(sc, &sc.Steinberger.MCPToolDesign, "mcp_tool_design", toolDesignScore, toolDesignScored)
+	recordOptionalScore(sc, &sc.Steinberger.MCPToolDesign, DimMCPToolDesign, toolDesignScore, toolDesignScored)
 	strategyScore, strategyScored := scoreMCPSurfaceStrategy(outputDir)
-	recordOptionalScore(sc, &sc.Steinberger.MCPSurfaceStrategy, "mcp_surface_strategy", strategyScore, strategyScored)
+	recordOptionalScore(sc, &sc.Steinberger.MCPSurfaceStrategy, DimMCPSurfaceStrategy, strategyScore, strategyScored)
 	sc.Steinberger.LocalCache = scoreLocalCache(outputDir)
 	cacheFreshnessScore, cacheFreshnessScored := scoreCacheFreshness(outputDir)
-	recordOptionalScore(sc, &sc.Steinberger.CacheFreshness, "cache_freshness", cacheFreshnessScore, cacheFreshnessScored)
+	recordOptionalScore(sc, &sc.Steinberger.CacheFreshness, DimCacheFreshness, cacheFreshnessScore, cacheFreshnessScored)
 	sc.Steinberger.Breadth = scoreBreadth(outputDir)
 	sc.Steinberger.Vision = scoreVision(outputDir)
 	sc.Steinberger.Workflows = scoreWorkflows(outputDir)
@@ -150,7 +169,7 @@ func recordOptionalScore(sc *Scorecard, target *int, dimension string, score int
 func scoreSpecDimensions(sc *Scorecard, outputDir, specPath string) error {
 	if specPath == "" {
 		// No spec: mark spec-dependent dimensions as unscored.
-		sc.UnscoredDimensions = append(sc.UnscoredDimensions, "path_validity", "auth_protocol")
+		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimPathValidity, DimAuthProtocol)
 		return nil
 	}
 
@@ -163,19 +182,19 @@ func scoreSpecDimensions(sc *Scorecard, outputDir, specPath string) error {
 		// Hand-built commands intentionally go beyond the spec; path-validity
 		// is not applicable. Mark unscored so the tier-2 denominator excludes
 		// it rather than awarding a 10-point cushion the CLI didn't earn.
-		sc.UnscoredDimensions = append(sc.UnscoredDimensions, "path_validity")
+		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimPathValidity)
 	} else {
 		pathValidity := evaluatePathValidity(outputDir, spec)
 		sc.Steinberger.PathValidity = pathValidity.score
 		if !pathValidity.scored {
-			sc.UnscoredDimensions = append(sc.UnscoredDimensions, "path_validity")
+			sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimPathValidity)
 		}
 	}
 
 	authProtocol := evaluateAuthProtocol(outputDir, spec)
 	sc.Steinberger.AuthProtocol = authProtocol.score
 	if !authProtocol.scored {
-		sc.UnscoredDimensions = append(sc.UnscoredDimensions, "auth_protocol")
+		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimAuthProtocol)
 	}
 	return nil
 }
@@ -195,7 +214,7 @@ func scoreDomainDimensions(sc *Scorecard, outputDir string, verifyReport *Verify
 	if liveScore, scored := scoreLiveAPIVerification(verifyReport); scored {
 		sc.Steinberger.LiveAPIVerification = liveScore
 	} else {
-		sc.UnscoredDimensions = append(sc.UnscoredDimensions, "live_api_verification")
+		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimLiveAPIVerification)
 	}
 }
 
@@ -893,7 +912,7 @@ func ApplyLiveCheckToScorecard(sc *Scorecard, live *LiveCheckResult) {
 		return
 	}
 	sc.Steinberger.LiveAPIVerification = score
-	sc.UnscoredDimensions = removeUnscoredDimension(sc.UnscoredDimensions, "live_api_verification")
+	sc.UnscoredDimensions = removeUnscoredDimension(sc.UnscoredDimensions, DimLiveAPIVerification)
 	recomputeScorecardTotals(sc)
 	applyScorecardCalibration(sc)
 	sc.OverallGrade = computeGrade(sc.Steinberger.Percentage)
@@ -954,7 +973,7 @@ func recomputeScorecardTotals(sc *Scorecard) {
 		sc.Steinberger.AgentWorkflow,
 	)
 
-	tier1Max := scorecardTierMax(sc, 200, "mcp_description_quality", "mcp_token_efficiency", "cache_freshness", "mcp_remote_transport", "mcp_tool_design", "mcp_surface_strategy")
+	tier1Max := scorecardTierMax(sc, 200, DimMCPDescriptionQuality, DimMCPTokenEfficiency, DimCacheFreshness, DimMCPRemoteTransport, DimMCPToolDesign, DimMCPSurfaceStrategy)
 	tier1Normalized := 0
 	if tier1Max > 0 {
 		tier1Normalized = (tier1Raw * 50) / tier1Max
@@ -970,7 +989,7 @@ func recomputeScorecardTotals(sc *Scorecard) {
 		sc.Steinberger.LiveAPIVerification,
 	)
 
-	tier2Max := scorecardTierMax(sc, 60, "live_api_verification", "path_validity", "auth_protocol")
+	tier2Max := scorecardTierMax(sc, 60, DimLiveAPIVerification, DimPathValidity, DimAuthProtocol)
 	tier2Normalized := 0
 	if tier2Max > 0 {
 		tier2Normalized = (tier2Raw * 50) / tier2Max
@@ -2386,15 +2405,15 @@ func buildGapReport(s SteinerScore, unscored []string) []string {
 		{"doctor", s.Doctor},
 		{"agent_native", s.AgentNative},
 		{"mcp_quality", s.MCPQuality},
-		{"mcp_description_quality", s.MCPDescriptionQuality},
-		{"mcp_token_efficiency", s.MCPTokenEff},
+		{DimMCPDescriptionQuality, s.MCPDescriptionQuality},
+		{DimMCPTokenEfficiency, s.MCPTokenEff},
 		{"local_cache", s.LocalCache},
 		{"breadth", s.Breadth},
 		{"vision", s.Vision},
 		{"workflows", s.Workflows},
 		{"insight", s.Insight},
-		{"path_validity", s.PathValidity},
-		{"auth_protocol", s.AuthProtocol},
+		{DimPathValidity, s.PathValidity},
+		{DimAuthProtocol, s.AuthProtocol},
 		{"data_pipeline_integrity", s.DataPipelineIntegrity},
 		{"sync_correctness", s.SyncCorrectness},
 		{"type_fidelity", s.TypeFidelity},
@@ -2517,15 +2536,15 @@ func writeScorecardMD(sc *Scorecard, pipelineDir string) error {
 		{"Doctor", "doctor", s.Doctor},
 		{"Agent Native", "agent_native", s.AgentNative},
 		{"MCP Quality", "mcp_quality", s.MCPQuality},
-		{"MCP Description Quality", "mcp_description_quality", s.MCPDescriptionQuality},
-		{"MCP Token Efficiency", "mcp_token_efficiency", s.MCPTokenEff},
+		{"MCP Description Quality", DimMCPDescriptionQuality, s.MCPDescriptionQuality},
+		{"MCP Token Efficiency", DimMCPTokenEfficiency, s.MCPTokenEff},
 		{"Local Cache", "local_cache", s.LocalCache},
 		{"Breadth", "breadth", s.Breadth},
 		{"Vision", "vision", s.Vision},
 		{"Workflows", "workflows", s.Workflows},
 		{"Insight", "insight", s.Insight},
-		{"Path Validity", "path_validity", s.PathValidity},
-		{"Auth Protocol", "auth_protocol", s.AuthProtocol},
+		{"Path Validity", DimPathValidity, s.PathValidity},
+		{"Auth Protocol", DimAuthProtocol, s.AuthProtocol},
 		{"Data Pipeline Integrity", "data_pipeline_integrity", s.DataPipelineIntegrity},
 		{"Sync Correctness", "sync_correctness", s.SyncCorrectness},
 	}

← 6586c0a2 feat(cli): per-resource base_url override in spec schema (#4  ·  back to Cli Printing Press  ·  fix(cli): dogfood agent-context discovery reads stdout only fe1e6d77 →