package pipeline

import (
	"bytes"
	"encoding/json"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"github.com/mvanhorn/cli-printing-press/v4/internal/devicespec"
	"github.com/mvanhorn/cli-printing-press/v4/internal/naming"
	"github.com/mvanhorn/cli-printing-press/v4/internal/openapi"
	apispec "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
	"gopkg.in/yaml.v3"
)

// infraCoreFiles are CLI infrastructure files excluded from workflow/insight scoring.
// These contain shared helpers and framework code, not individual commands.
var infraCoreFiles = map[string]bool{
	"helpers.go": true, "root.go": true, "doctor.go": true, "auth.go": true,
}

// infraAllFiles extends infraCoreFiles with vision/data-layer commands that are
// scored by their own dedicated dimensions (vision, sync_correctness, etc.)
// and should not be double-counted by breadth or sampled as generic commands.
var infraAllFiles = map[string]bool{
	"helpers.go": true, "root.go": true, "doctor.go": true, "auth.go": true,
	"export.go": true, "import.go": true, "search.go": true, "sync.go": true,
	"tail.go": true, "analytics.go": true,
}

var actionableDoctorSuggestionRE = regexp.MustCompile(`(?i)\b(run|try)\b.{0,80}\bdoctor\b`)
var clientAPICallRE = regexp.MustCompile(`c\.(Get|Post|Put|Delete|Patch)\s*\(`)
var resourcesSQLSearchRE = regexp.MustCompile(`(?is)\bSELECT\b.*\bFROM\s+resources\b.*\bresource_type\b`)
var resourcesFTSSQLSearchRE = regexp.MustCompile(`(?is)\bSELECT\b.*\bresources_fts\b.*\bresource_type\b`)
var sqlQueryCallRE = regexp.MustCompile(`\.\s*Query(Row)?\s*\(`)
var quotaDailySignalRE = regexp.MustCompile(`(?i)\b(daily|per[-_\s]+day)\b`)

// Scorecard holds the auto-scored evaluation of a generated CLI against the Steinberger bar.
type Scorecard struct {
	APIName                     string                      `json:"api_name"`
	Steinberger                 SteinerScore                `json:"steinberger"`
	CompetitorScores            []CompScore                 `json:"competitor_scores"`
	OverallGrade                string                      `json:"overall_grade"`
	GapReport                   []string                    `json:"gap_report"`
	UnscoredDimensions          []string                    `json:"unscored_dimensions,omitempty"`
	NovelFeatureDepthMismatches []NovelFeatureDepthMismatch `json:"novel_feature_depth_mismatches,omitempty"`

	verifyCalibrationFloor   int
	browserSessionUnverified bool
}

// SteinerScore breaks down the Steinberger bar into 11 dimensions, each 0-10.
type SteinerScore struct {
	OutputModes           int `json:"output_modes"`             // 0-10
	Auth                  int `json:"auth"`                     // 0-10
	ErrorHandling         int `json:"error_handling"`           // 0-10
	TerminalUX            int `json:"terminal_ux"`              // 0-10
	README                int `json:"readme"`                   // 0-10
	Doctor                int `json:"doctor"`                   // 0-10
	AgentNative           int `json:"agent_native"`             // 0-10
	MCPQuality            int `json:"mcp_quality"`              // 0-10
	MCPDescriptionQuality int `json:"mcp_description_quality"`  // 0-10; unscored when no tools-manifest.json. Penalizes thin per-tool descriptions (the same threshold as `cli-printing-press tools-audit` thin-mcp-description).
	MCPTokenEff           int `json:"mcp_token_efficiency"`     // 0-10; unscored when no MCP surface
	MCPRemoteTransport    int `json:"mcp_remote_transport"`     // 0-10; unscored when no MCP surface or small endpoint mirrors. Rewards remote-capable MCP servers.
	MCPToolDesign         int `json:"mcp_tool_design"`          // 0-10; unscored when no MCP surface or endpoint count below mcpEnrichmentMinEndpoints. Rewards intent-grouped tools vs. endpoint mirrors.
	MCPSurfaceStrategy    int `json:"mcp_surface_strategy"`     // 0-10; unscored unless the endpoint surface exceeds surfaceStrategyLargeThreshold or code-orchestration is explicitly used. Penalizes endpoint-mirror at scale.
	LocalCache            int `json:"local_cache"`              // 0-10
	CacheFreshness        int `json:"cache_freshness"`          // 0-10; unscored when the CLI has no local store
	Breadth               int `json:"breadth"`                  // 0-10: how many commands (penalizes empty CLIs)
	Vision                int `json:"vision"`                   // 0-10
	Workflows             int `json:"workflows"`                // 0-10
	Insight               int `json:"insight"`                  // 0-10
	AgentWorkflow         int `json:"agent_workflow_readiness"` // 0-10: HeyGen-derived - async jobs, profiles, deliver, feedback
	// Tier 2: Domain Correctness (semantic checks)
	PathValidity          int    `json:"path_validity"`           // 0-10
	AuthProtocol          int    `json:"auth_protocol"`           // 0-10
	DataPipelineIntegrity int    `json:"data_pipeline_integrity"` // 0-10
	SyncCorrectness       int    `json:"sync_correctness"`        // 0-10
	TypeFidelity          int    `json:"type_fidelity"`           // 0-5
	DeadCode              int    `json:"dead_code"`               // 0-5
	LiveAPIVerification   int    `json:"live_api_verification"`   // 0-10; unscored when verify ran in mock/structural mode or was skipped
	Total                 int    `json:"total"`                   // 0-100 (weighted: 50% infrastructure + 50% domain)
	Percentage            int    `json:"percentage"`              // 0-100
	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"
	DimSyncCorrectness       = "sync_correctness"
	DimTypeFidelity          = "type_fidelity"
	DimDeadCode              = "dead_code"
	DimLiveAPIVerification   = "live_api_verification"
	// HTTP-API-shaped dimensions that do not apply to a BLE device CLI (no remote
	// API, no sync->sql->search pipeline, no response cache). Marked N/A for
	// device CLIs so they drop from the denominator instead of scoring a false 0.
	DimLocalCache            = "local_cache"
	DimVision                = "vision"
	DimWorkflows             = "workflows"
	DimInsight               = "insight"
	DimAgentWorkflow         = "agent_workflow_readiness"
	DimDataPipelineIntegrity = "data_pipeline_integrity"
)

// CompScore compares our score against a competitor on a single dimension.
type CompScore struct {
	Name       string `json:"name"`
	OurScore   int    `json:"our_score"`
	TheirScore int    `json:"their_score"`
	WeWin      bool   `json:"we_win"`
}

// RunScorecard evaluates generated CLI files and produces a scorecard.
// If verifyReport is non-nil, verify results calibrate the final score.
func RunScorecard(outputDir, pipelineDir, specPath string, verifyReport *VerifyReport) (*Scorecard, error) {
	// Strip the CLI suffix because outputDir from fullrun (paths.WorkingCLIDir)
	// and library checkouts both end in -pp-cli; APIName is the API slug,
	// not the binary name, and lands in user-visible output (Markdown
	// scorecard header, "Quality Scorecard:" CLI line).
	sc := &Scorecard{APIName: naming.TrimCLISuffix(filepath.Base(outputDir))}

	if err := scoreScorecardDimensions(sc, outputDir, specPath, verifyReport); err != nil {
		return nil, err
	}
	finalizeScorecard(sc, outputDir, pipelineDir, verifyReport)

	if err := writeScorecardArtifacts(sc, pipelineDir); err != nil {
		return sc, err
	}

	return sc, nil
}

func scoreScorecardDimensions(sc *Scorecard, outputDir, specPath string, verifyReport *VerifyReport) error {
	isDevice := isDeviceBackedCLIDir(outputDir) || looksLikeDeviceSpecFile(specPath)
	scoreInfrastructureDimensions(sc, outputDir, isDevice)
	spec, err := scoreSpecDimensions(sc, outputDir, specPath)
	if err != nil {
		return err
	}
	scoreDomainDimensions(sc, outputDir, spec, verifyReport, isDevice)
	return nil
}

func scoreInfrastructureDimensions(sc *Scorecard, outputDir string, isDevice bool) {
	reachableInternalFiles := scorecardReachableInternalFiles(outputDir)
	reachableInternalContent := scorecardContentsFromFiles(reachableInternalFiles)
	sc.Steinberger.Auth = scoreAuth(outputDir)
	if isDevice {
		sc.Steinberger.OutputModes = scoreOutputModesDevice(outputDir)
		sc.Steinberger.ErrorHandling = scoreErrorHandlingDevice(reachableInternalContent)
		sc.Steinberger.TerminalUX = scoreTerminalUXDevice(outputDir)
		sc.Steinberger.README = scoreREADMEDevice(outputDir)
		sc.Steinberger.Doctor = scoreDoctorDevice(outputDir)
		sc.Steinberger.AgentNative = scoreAgentNativeDevice(outputDir)
	} else {
		sc.Steinberger.OutputModes = scoreOutputModesWithSurface(outputDir, reachableInternalContent, reachableInternalFiles)
		sc.Steinberger.ErrorHandling = scoreErrorHandlingFromSurface(reachableInternalContent)
		sc.Steinberger.TerminalUX = scoreTerminalUXWithSurface(outputDir, reachableInternalContent)
		sc.Steinberger.README = scoreREADME(outputDir)
		sc.Steinberger.Doctor = scoreDoctor(outputDir)
		sc.Steinberger.AgentNative = scoreAgentNative(outputDir)
	}
	sc.Steinberger.MCPQuality = scoreMCPQuality(outputDir)
	mcpDescScore, mcpDescScored := scoreMCPDescriptionQuality(outputDir)
	recordOptionalScore(sc, &sc.Steinberger.MCPDescriptionQuality, DimMCPDescriptionQuality, mcpDescScore, mcpDescScored)
	mcpTokenScore, mcpTokenScored := scoreMCPTokenEfficiency(outputDir)
	recordOptionalScore(sc, &sc.Steinberger.MCPTokenEff, DimMCPTokenEfficiency, mcpTokenScore, mcpTokenScored)
	remoteScore, remoteScored := scoreMCPRemoteTransport(outputDir)
	recordOptionalScore(sc, &sc.Steinberger.MCPRemoteTransport, DimMCPRemoteTransport, remoteScore, remoteScored)
	toolDesignScore, toolDesignScored := scoreMCPToolDesign(outputDir)
	recordOptionalScore(sc, &sc.Steinberger.MCPToolDesign, DimMCPToolDesign, toolDesignScore, toolDesignScored)
	strategyScore, strategyScored := scoreMCPSurfaceStrategy(outputDir)
	recordOptionalScore(sc, &sc.Steinberger.MCPSurfaceStrategy, DimMCPSurfaceStrategy, strategyScore, strategyScored)
	cacheFreshnessScore, cacheFreshnessScored := scoreCacheFreshness(outputDir)
	recordOptionalScore(sc, &sc.Steinberger.CacheFreshness, DimCacheFreshness, cacheFreshnessScore, cacheFreshnessScored)
	if isDevice {
		sc.Steinberger.Breadth = scoreBreadthDevice(outputDir)
	} else {
		sc.Steinberger.Breadth = scoreBreadth(outputDir)
	}
	if isDevice {
		// A BLE device CLI wraps no remote API and has no HTTP-shaped local cache,
		// sync-driven datastore, or multi-source aggregation surface. Mark these
		// N/A so they drop from the denominator rather than scoring a false 0.
		sc.UnscoredDimensions = append(sc.UnscoredDimensions,
			DimLocalCache, DimVision, DimWorkflows, DimInsight, DimAgentWorkflow)
	} else {
		sc.Steinberger.LocalCache = scoreLocalCache(outputDir)
		sc.Steinberger.Vision = scoreVision(outputDir)
		sc.Steinberger.Workflows = scoreWorkflows(outputDir)
		sc.Steinberger.Insight = scoreInsight(outputDir)
		sc.Steinberger.AgentWorkflow = scoreAgentWorkflow(outputDir)
	}
}

func recordOptionalScore(sc *Scorecard, target *int, dimension string, score int, scored bool) {
	if scored {
		*target = score
		return
	}
	sc.UnscoredDimensions = append(sc.UnscoredDimensions, dimension)
}

func scoreSpecDimensions(sc *Scorecard, outputDir, specPath string) (*openAPISpecInfo, error) {
	if isDeviceBackedCLIDir(outputDir) || looksLikeDeviceSpecFile(specPath) {
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimPathValidity, DimAuthProtocol)
		return nil, nil
	}
	if isLocalDatastoreCLIDir(outputDir) {
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimPathValidity, DimAuthProtocol)
		return nil, nil
	}
	specPath = scorecardSpecPath(outputDir, specPath)
	if specPath == "" {
		// No spec: mark spec-dependent dimensions as unscored.
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimPathValidity, DimAuthProtocol)
		return nil, nil
	}

	spec, err := loadOpenAPISpec(specPath)
	if err != nil {
		return nil, err
	}

	if spec.IsSynthetic() || spec.IsGraphQL {
		// Hand-built commands intentionally go beyond the spec, and GraphQL
		// CLIs expose semantic command paths over one POST endpoint. In both
		// cases 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, DimPathValidity)
	} else {
		pathValidity := evaluatePathValidity(outputDir, spec)
		sc.Steinberger.PathValidity = pathValidity.score
		if !pathValidity.scored {
			sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimPathValidity)
		}
	}

	authProtocol := evaluateAuthProtocol(outputDir, spec)
	sc.Steinberger.AuthProtocol = authProtocol.score
	if !authProtocol.scored {
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimAuthProtocol)
	}
	return spec, nil
}

func scorecardSpecPath(outputDir, specPath string) string {
	embedded := filepath.Join(outputDir, "spec.json")
	if fileExists(embedded) {
		return embedded
	}
	return specPath
}

func scoreDomainDimensions(sc *Scorecard, outputDir string, spec *openAPISpecInfo, verifyReport *VerifyReport, isDevice bool) {
	if isDevice {
		// BLE device CLIs have no sync->sql->search data pipeline; the HTTP-shaped
		// pipeline and sync checks don't apply. Mark N/A rather than scoring 0.
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimDataPipelineIntegrity, DimSyncCorrectness)
	} else if !hasScorecardLocalStore(outputDir) {
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimDataPipelineIntegrity, DimSyncCorrectness)
	} else {
		sc.Steinberger.DataPipelineIntegrity = scoreDataPipelineIntegrity(outputDir)
		if isLocalDatastoreCLIDir(outputDir) {
			sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimSyncCorrectness)
		} else {
			sc.Steinberger.SyncCorrectness = scoreSyncCorrectness(outputDir)
		}
	}
	if isDevice {
		sc.Steinberger.TypeFidelity = scoreTypeFidelityDevice(outputDir)
	} else if !hasScorecardLocalStore(outputDir) {
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimTypeFidelity)
	} else {
		sc.Steinberger.TypeFidelity = scoreTypeFidelity(outputDir, spec)
	}
	sc.Steinberger.DeadCode = scoreDeadCode(outputDir)

	// LiveAPIVerification is scored only when verify ran in live mode (real
	// API, not a mock server and not structural-only). Mock-backed verify
	// passes and live verify passes look identical in dogfood output; making
	// this a distinct dimension in the scorecard lets reviewers tell them
	// apart at a glance and gives selfimprove a targeted fix plan when a
	// shipped CLI has never been exercised against the real API.
	if liveScore, scored := scoreLiveAPIVerification(verifyReport); scored {
		sc.Steinberger.LiveAPIVerification = liveScore
	} else {
		sc.UnscoredDimensions = append(sc.UnscoredDimensions, DimLiveAPIVerification)
	}
}

func finalizeScorecard(sc *Scorecard, outputDir, pipelineDir string, verifyReport *VerifyReport) {
	browserSessionUnverified := verifyReport != nil && verifyReport.BrowserSessionRequired && verifyReport.BrowserSessionProof != "valid"
	sc.browserSessionUnverified = browserSessionUnverified
	if browserSessionUnverified {
		if sc.Steinberger.Auth > 5 {
			sc.Steinberger.Auth = 5
		}
		if sc.Steinberger.AuthProtocol > 5 {
			sc.Steinberger.AuthProtocol = 5
		}
	}

	// Apply verify caps to dimensions BEFORE tier calculation so Total stays consistent
	if verifyReport != nil {
		if !verifyReport.DataPipeline && sc.Steinberger.DataPipelineIntegrity > 5 {
			sc.Steinberger.DataPipelineIntegrity = 5
		}
	}

	if verifyReport != nil {
		verifyScore := int(verifyReport.PassRate)
		sc.verifyCalibrationFloor = (verifyScore * 80) / 100 // 91% verify -> 72 floor
	}
	recomputeScorecardTotals(sc)
	applyScorecardCalibration(sc)

	// Grade
	sc.OverallGrade = computeGrade(sc.Steinberger.Percentage)

	// Gap report for dimensions below 5
	sc.GapReport = buildGapReport(sc.Steinberger, sc.UnscoredDimensions)
	sc.NovelFeatureDepthMismatches = scorecardNovelFeatureDepthMismatches(outputDir, pipelineDir)
	appendNovelFeatureDepthGaps(sc)

	// MCP tool split from manifest (informational, does not affect score)
	if manifest, err := loadCLIManifestForScorecard(outputDir); err == nil && manifest.MCPBinary != "" {
		authCount := manifest.MCPToolCount - manifest.MCPPublicToolCount
		sc.GapReport = append(sc.GapReport,
			fmt.Sprintf("MCP: %d tools (%d public, %d auth-required) — readiness: %s",
				manifest.MCPToolCount, manifest.MCPPublicToolCount, authCount, manifest.MCPReady))
	}

	// Competitor comparison from research.json
	sc.CompetitorScores = buildCompetitorScores(sc.Steinberger.Total, pipelineDir)
}

func writeScorecardArtifacts(sc *Scorecard, pipelineDir string) error {
	if err := writeScorecardMD(sc, pipelineDir); err != nil {
		return fmt.Errorf("writing scorecard.md: %w", err)
	}
	if err := writeScorecardJSON(sc, pipelineDir); err != nil {
		return fmt.Errorf("writing scorecard.json: %w", err)
	}
	return nil
}

func (sc *Scorecard) IsDimensionUnscored(name string) bool {
	return slices.Contains(sc.UnscoredDimensions, name)
}

func scoreOutputModes(dir string) int {
	reachableFiles := scorecardReachableInternalFiles(dir)
	return scoreOutputModesWithSurface(dir, scorecardContentsFromFiles(reachableFiles), reachableFiles)
}

func scoreOutputModesWithSurface(dir string, surfaceContent []string, surfaceFiles []string) int {
	rootContent := readFileContent(filepath.Join(dir, "internal", "cli", "root.go"))
	score := 0
	// Presence tier (max 5)
	if strings.Contains(rootContent, `"json"`) {
		score += 1
	}
	if strings.Contains(rootContent, `"plain"`) {
		score += 1
	}
	if strings.Contains(rootContent, `"select"`) {
		score += 1
	}
	if strings.Contains(rootContent, `"csv"`) {
		score += 1
	}
	if strings.Contains(rootContent, `"quiet"`) {
		score += 1
	}
	// Quality tier: field-aware select (real JSON parsing, not string ops)
	if containsAllInAny(surfaceContent, "filterFields", "json.Unmarshal") {
		score += 2
	}
	// Quality tier: pagination progress events
	if containsAnyInAny(surfaceContent, "page_fetch", "ndjson") || hasPageProgressStructureInFiles(surfaceFiles) {
		score += 1
	}
	// Quality tier: tabwriter for aligned output
	if containsAnyInAny(surfaceContent, "tabwriter") {
		score += 2
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreAuth(dir string) int {
	configContent := readFileContent(filepath.Join(dir, "internal", "config", "config.go"))
	authContent := readFileContent(filepath.Join(dir, "internal", "cli", "auth.go"))
	clientContent := readFileContent(filepath.Join(dir, "internal", "client", "client.go"))

	// No-auth exemption: when the generator decided this spec requires no
	// auth surface, internal/cli/auth.go is not emitted. (Specs declaring
	// auth.type: none with no AuthorizationURL and no graphql_persisted_query
	// hint take this path; see Generator.shouldEmitAuth.) Penalizing such
	// CLIs for "missing auth subcommand" or "no env vars" is wrong --
	// they correctly reflect a no-auth spec. Award full credit and let the
	// auth-bearing dimensions on auth-bearing CLIs speak for themselves.
	//
	// Note: readFileContent returns "" for both "file does not exist" and
	// "file exists but is empty." We rely on the generator never emitting an
	// empty auth.go -- every auth template (auth.go.tmpl, auth_simple.go.tmpl,
	// auth_browser.go.tmpl) produces a non-empty file with at least the cobra
	// command stub. If a future code path emits an empty auth.go as a
	// placeholder, this exemption would fire incorrectly; the safer signal
	// is os.Stat for file existence, but the empty-content check matches
	// today's behavior with one fewer syscall.
	if authContent == "" {
		return 10
	}

	score := 0
	// Auth pattern: generated config reads credentials with os.Getenv.
	if strings.Count(configContent, "os.Getenv") >= 1 {
		score += 2
	}
	// Presence: auth file exists (we already returned above when it doesn't)
	score += 1
	// Quality: secure config file permissions (0o600 or 0600)
	if strings.Contains(configContent, "0o600") || strings.Contains(configContent, "0600") || strings.Contains(configContent, "0o700") || strings.Contains(configContent, "0700") {
		score += 2
	}
	// Auth pattern: client code masks credentials before printing.
	if strings.Contains(clientContent, "mask") || strings.Contains(clientContent, "***") || strings.Contains(clientContent, "last 4") || (strings.Contains(clientContent, "Authorization") && strings.Contains(clientContent, "[:")) {
		score += 2
	}
	// Auth pattern: config supports multiple credential sources.
	authSources := 0
	if strings.Contains(configContent, "os.Getenv") {
		authSources++
	}
	if strings.Contains(configContent, "ReadFile") || strings.Contains(configContent, "Load") {
		authSources++
	}
	if authSources >= 2 {
		score += 1
	}
	// TODO: Replace this free grant with real OAuth2 scoring when the generator
	// can produce OAuth2 browser flows from spec authorizationCode grants.
	// Auto-award 2 points so the ceiling is 10/10 for what's currently possible.
	score += 2
	if score > 10 {
		score = 10
	}
	return score
}

func scoreErrorHandling(dir string) int {
	return scoreErrorHandlingFromSurface(scorecardReachableInternalContents(dir))
}

func scoreErrorHandlingFromSurface(surfaceContent []string) int {
	score := 0
	// Presence: error hints
	if containsAnyInAny(surfaceContent, "hint:", "Hint:") {
		score += 1
	}
	// Presence: at least 3 distinct exit codes
	exitCount := countAcross(surfaceContent, "code:")
	if exitCount >= 3 {
		score += 2
	} else if exitCount >= 1 {
		score += 1
	}
	// Quality: rate limit handling (429 + retry)
	if containsAllInAny(surfaceContent, "429", "Retry-After") || containsAllInAny(surfaceContent, "429", "backoff") || containsAllInAny(surfaceContent, "429", "retry") {
		score += 2
	}
	// Quality: idempotency (409 = already exists = success)
	if containsAllInAny(surfaceContent, "409", "already exists") {
		score += 2
	}
	// Quality: 404 with specific exit code
	if containsAnyInAny(surfaceContent, "404") {
		score += 1
	}
	// Excellence: actionable suggestions in errors (not just codes)
	if containsActionableDoctorSuggestion(surfaceContent) {
		score += 2
	}
	if score > 10 {
		score = 10
	}
	return score
}

func containsActionableDoctorSuggestion(surfaceContent []string) bool {
	return slices.ContainsFunc(surfaceContent, actionableDoctorSuggestionRE.MatchString)
}

func scoreTerminalUX(dir string) int {
	return scoreTerminalUXWithSurface(dir, scorecardReachableInternalContents(dir))
}

func scoreTerminalUXWithSurface(dir string, surfaceContent []string) int {
	rootContent := readFileContent(filepath.Join(dir, "internal", "cli", "root.go"))
	score := 0
	// Presence: NO_COLOR support
	if containsAnyInAny(surfaceContent, "NO_COLOR") {
		score += 1
	}
	// Presence: TTY detection. Accept any canonical Go idiom:
	// ModeCharDevice (the generator's helpers.go template), IsTerminal /
	// x/term (golang.org/x/term), or isatty (github.com/mattn/go-isatty).
	if containsAnyInAny(surfaceContent, "isatty", "IsTerminal", "x/term", "ModeCharDevice") {
		score += 1
	}
	// Presence: no-color flag
	if strings.Contains(rootContent, "no-color") {
		score += 1
	}
	// Quality: tabwriter for aligned columns
	if containsAnyInAny(surfaceContent, "tabwriter") {
		score += 2
	}
	// Quality: help text descriptions are meaningful (not just verb names)
	cmdFiles := sampleCommandFiles(dir, 5)
	goodDescs := 0
	for _, content := range cmdFiles {
		if hasQualityDescription(content) {
			goodDescs++
		}
	}
	if goodDescs >= 4 {
		score += 2
	} else if goodDescs >= 2 {
		score += 1
	}
	// Quality: example values are realistic (not abc123 or bare "value")
	goodExamples := 0
	for _, content := range cmdFiles {
		if !hasPlaceholderValues(content) {
			goodExamples++
		}
	}
	if goodExamples >= 4 {
		score += 3
	} else if goodExamples >= 2 {
		score += 1
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreREADME(dir string) int {
	content := readFileContent(filepath.Join(dir, "README.md"))
	score := 0
	// Presence: key sections exist (1pt each, max 4)
	// Each entry can have aliases (e.g., "Doctor" and "Health Check" mean the same thing)
	for _, aliases := range [][]string{{"Quick Start"}, {"Agent Usage"}, {"Doctor", "Health Check"}, {"Troubleshooting"}} {
		for _, section := range aliases {
			if strings.Contains(content, section) {
				score++
				break
			}
		}
	}
	// Quality: Quick Start has no obvious placeholder/template values.
	// "your-key-here" in an export line is a legitimate auth setup example,
	// not a sign of unfinished boilerplate. Only penalize generic resource
	// placeholders like "abc123" or unresolved template markers like "USER/tap".
	qsIdx := strings.Index(content, "Quick Start")
	if qsIdx >= 0 {
		qsSection := content[qsIdx:min(qsIdx+500, len(content))]
		if !strings.Contains(qsSection, "USER/tap") && !strings.Contains(qsSection, "abc123") {
			score += 2
		}
	}
	// Quality: has Cookbook or Recipes with 3+ code blocks
	if strings.Contains(content, "Cookbook") || strings.Contains(content, "Recipes") {
		codeBlocks := strings.Count(content, "```")
		if codeBlocks >= 6 { // 3+ examples = 6+ backtick pairs
			score += 2
		} else {
			score += 1
		}
	}
	// Quality: README describes the API in human terms (not raw spec text)
	lines := strings.SplitN(content, "\n", 5)
	if len(lines) >= 3 {
		header := strings.Join(lines[:3], " ")
		if !strings.Contains(header, "Preview of") && !strings.Contains(header, "specification") && len(header) > 20 {
			score += 2
		}
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreDoctor(dir string) int {
	content := readFileContent(filepath.Join(dir, "internal", "cli", "doctor.go"))
	if content == "" {
		return 0
	}
	localDatastore := isLocalDatastoreCLIDir(dir)
	score := 0
	// Presence: doctor command exists
	score += 2
	// Quality: checks auth/token validity
	if strings.Contains(content, "auth") || strings.Contains(content, "token") || strings.Contains(content, "Token") {
		score += 2
	}
	// Quality: checks API connectivity (makes an HTTP request)
	if hasDoctorHTTPReachability(content) || (localDatastore && hasLocalDatastoreReachability(content)) {
		score += 2
	}
	// Quality: checks config file
	if strings.Contains(content, "config") || strings.Contains(content, "Config") {
		score += 2
	}
	// Excellence: checks version or API compatibility
	if strings.Contains(content, "version") || strings.Contains(content, "Version") {
		score += 2
	}
	if score > 10 {
		score = 10
	}
	return score
}

func hasDoctorHTTPReachability(content string) bool {
	if strings.Contains(content, "http.Get") ||
		strings.Contains(content, "http.Head") ||
		strings.Contains(content, "http.Post") ||
		strings.Contains(content, "http.NewRequest") {
		return true
	}
	clientCallRe := regexp.MustCompile(`\b[A-Za-z_]\w*(?:Client|HTTPClient)?\.(?:Get|Head|Post|Put|Patch|Delete|Do)\s*\(`)
	inlineClientCallRe := regexp.MustCompile(`\(&http\.Client\s*\{[^}]*\}\)\.(?:Get|Head|Post|Put|Patch|Delete|Do)\s*\(`)
	return clientCallRe.MatchString(content) || inlineClientCallRe.MatchString(content)
}

func hasLocalDatastoreReachability(content string) bool {
	lower := strings.ToLower(content)
	hasSQLiteSignal := strings.Contains(lower, "sqlite") || strings.Contains(lower, "database/sql")
	if !hasSQLiteSignal {
		return false
	}
	return strings.Contains(lower, "sql.open") ||
		strings.Contains(lower, ".ping(") ||
		strings.Contains(lower, ".query(") ||
		strings.Contains(lower, ".queryrow(") ||
		strings.Contains(lower, ".exec(")
}

// scoreDoctorDevice scores a BLE device CLI's doctor command. Device doctors
// live in any cli file (not a dedicated doctor.go) and check BLE reachability,
// build/verify state, and device metadata rather than HTTP auth/connectivity.
// It reads the whole cli package directly because the doctor command may sit in
// a file the HTTP-shaped reachability surface does not include.
func scoreDoctorDevice(dir string) int {
	joined := deviceCLIContent(dir)
	if !strings.Contains(joined, `"doctor"`) {
		return 0
	}
	lower := strings.ToLower(joined)
	score := 2 // doctor command present
	if hasDeviceReachability(joined) {
		score += 2 // probes whether the device is reachable
	}
	if strings.Contains(lower, "verify") || strings.Contains(lower, "dogfood") || strings.Contains(lower, "compiled") {
		score += 2 // reports build / verify-mode state
	}
	if strings.Contains(lower, "address") || strings.Contains(lower, "config") {
		score += 2 // device address / config handling
	}
	if strings.Contains(lower, "capabilit") || strings.Contains(lower, "serviceuuid") || strings.Contains(lower, "service_uuid") {
		score += 2 // surfaces device/service metadata
	}
	if score > 10 {
		score = 10
	}
	return score
}

func hasDeviceReachability(content string) bool {
	lower := strings.ToLower(content)
	return strings.Contains(lower, ".scan(") ||
		strings.Contains(lower, "reachable") ||
		strings.Contains(lower, "available()")
}

// scoreErrorHandlingDevice scores error handling for a BLE device CLI. Device
// errors are protocol- and safety-shaped (out-of-range inputs, "not found",
// "pass --live", confirmation gating) rather than HTTP status codes.
func scoreErrorHandlingDevice(surfaceContent []string) int {
	score := 0
	if countAcross(surfaceContent, "code:") >= 1 || containsAnyInAny(surfaceContent, "pp:typed-exit-codes") {
		score += 2 // typed / documented exit codes
	}
	if containsAnyInAny(surfaceContent, "out of range", "not found", "must be") {
		score += 2 // specific, input-naming validation errors
	}
	if containsAnyInAny(surfaceContent, "%w") {
		score += 2 // wraps underlying errors for propagation
	}
	if containsAnyInAny(surfaceContent, "pass --live", "wp_live", "ErrLiveUnavailable", "official", "--confirm") {
		score += 3 // actionable device/safety remediation guidance
	}
	if containsActionableDoctorSuggestion(surfaceContent) {
		score += 1
	}
	if score > 10 {
		score = 10
	}
	return score
}

// The scorers below grade the HTTP-shaped Steinberger dimensions on a BLE device
// CLI's actual shape. The HTTP variants key off structure a device CLI does not
// have -- separate per-command files, HTTP README sections, endpoint counts, API
// response types -- and so score a healthy device CLI a false 0. A device CLI
// keeps its commands in root.go, outputs --json/--agent + text, ships device
// README sections, and exposes its agent surface via the MCP server.

func deviceCLIContent(dir string) string {
	return readAllGoFiles(filepath.Join(dir, "internal", "cli"))
}

func scoreOutputModesDevice(dir string) int {
	cli := deviceCLIContent(dir)
	score := 0
	if strings.Contains(cli, `"json"`) {
		score += 3 // structured machine output
	}
	if strings.Contains(cli, `"agent"`) {
		score += 2 // agent-friendly JSON alias
	}
	if strings.Contains(cli, "writeJSON(") {
		score += 3 // per-command JSON emission
	}
	if strings.Contains(cli, "OutOrStdout()") {
		score += 2 // human-readable text output
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreTerminalUXDevice(dir string) int {
	cli := deviceCLIContent(dir)
	score := 0
	if strings.Contains(cli, "OutOrStdout()") {
		score += 3 // human-readable text output
	}
	if strings.Contains(cli, `"json"`) {
		score += 2 // structured alternative for non-TTY use
	}
	switch shortCount := strings.Count(cli, "Short:"); {
	case shortCount >= 4:
		score += 3 // most commands carry a meaningful one-line description
	case shortCount >= 2:
		score += 1
	}
	if strings.Contains(cli, "NO_COLOR") || strings.Contains(cli, "IsTerminal") {
		score += 2 // optional color / TTY awareness
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreREADMEDevice(dir string) int {
	content := readFileContent(filepath.Join(dir, "README.md"))
	score := 0
	for _, section := range []string{"## Commands", "## Live control", "## MCP server"} {
		if strings.Contains(content, section) {
			score += 2
		}
	}
	if strings.Contains(content, "-tags ble_live") {
		score += 1 // documents the live build
	}
	if strings.Contains(content, "capabilities") {
		score += 1 // points at the capability/safety surface
	}
	if strings.Contains(content, "device-native") || strings.Contains(content, "BLE device spec") {
		score += 1
	}
	if len(content) >= 400 {
		score += 1
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreAgentNativeDevice(dir string) int {
	cli := deviceCLIContent(dir)
	score := 0
	if strings.Contains(cli, `"json"`) {
		score += 2
	}
	if strings.Contains(cli, `"agent"`) {
		score += 2
	}
	if strings.Contains(cli, "dry-run") {
		score += 1
	}
	// Agent-native parity: an MCP server mirrors the command surface as tools.
	if _, err := os.Stat(filepath.Join(dir, "internal", "mcp")); err == nil {
		score += 3
	}
	if strings.Contains(cli, `"capabilities"`) {
		score += 1 // machine-readable capability + safety discovery
	}
	if !strings.Contains(cli, "bufio.NewScanner(os.Stdin)") && !strings.Contains(cli, "ReadString(") {
		score += 1 // non-interactive
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreBreadthDevice(dir string) int {
	// Count registered commands across the cli package (generated built-ins,
	// generated device commands, and hand-authored novel commands) plus telemetry
	// fields -- breadth scales with how much of the device the CLI exposes.
	commands := strings.Count(deviceCLIContent(dir), "AddCommand(")
	telemetry := strings.Count(readFileContent(filepath.Join(dir, "internal", "device", "spec.go")), "SourceCharacteristicUUID:")
	switch total := commands + telemetry; {
	case total >= 14:
		return 10
	case total >= 10:
		return 8
	case total >= 7:
		return 6
	case total >= 5:
		return 4
	case total >= 3:
		return 2
	default:
		return 1
	}
}

func scoreTypeFidelityDevice(dir string) int {
	spec := readFileContent(filepath.Join(dir, "internal", "device", "spec.go"))
	transport := readFileContent(filepath.Join(dir, "internal", "device", "transport.go"))
	score := 0
	// Device tier 2 removes HTTP-only dimensions, leaving Type Fidelity and
	// Dead Code as the ten remaining points. Keep this scorer on the same
	// five-point scale as the generic scorer; Dead Code supplies the other five.
	if strings.Contains(spec, "type CommandDefinition struct") {
		score++ // typed command model
	}
	if strings.Contains(spec, "type StatusField struct") {
		score++ // typed telemetry model
	}
	if strings.Contains(spec, "type CapabilitySummary struct") {
		score++ // typed capability summary
	}
	if strings.Contains(transport, "type CommandResult struct") {
		score++ // typed command result
	}
	if strings.Contains(spec, `Parameters: []string{"`) {
		score++ // a command declares typed parameters
	}
	return score
}

func scoreAgentNative(dir string) int {
	rootContent := readFileContent(filepath.Join(dir, "internal", "cli", "root.go"))
	helpersContent := readFileContent(filepath.Join(dir, "internal", "cli", "helpers.go"))
	score := 0
	// Presence: core agent flags (1pt each, max 5)
	if strings.Contains(rootContent, `"json"`) {
		score++
	}
	if strings.Contains(rootContent, `"select"`) {
		score++
	}
	if strings.Contains(rootContent, "dry-run") {
		score++
	}
	if strings.Contains(rootContent, "stdin") {
		score++
	}
	if strings.Contains(rootContent, `"yes"`) {
		score++
	}
	// Quality: non-interactive (no prompts in command files)
	cmdFiles := sampleCommandFiles(dir, 5)
	hasPrompts := false
	for _, content := range cmdFiles {
		if strings.Contains(content, "bufio.NewScanner(os.Stdin)") || strings.Contains(content, "Prompt") || strings.Contains(content, "ReadLine") {
			hasPrompts = true
			break
		}
	}
	if !hasPrompts && len(cmdFiles) > 0 {
		score++
	}
	// Quality: typed exit codes (5+ distinct)
	exitCount := strings.Count(helpersContent, "code:")
	if exitCount >= 5 {
		score += 2
	} else if exitCount >= 3 {
		score++
	}
	// Excellence: --stdin examples in command files (at least 3 commands show stdin usage)
	stdinExamples := 0
	for _, content := range cmdFiles {
		if strings.Contains(content, "--stdin") && strings.Contains(content, "Example") {
			stdinExamples++
		}
	}
	// Also check all command files for stdin examples, not just sample
	allCmdFiles := sampleCommandFiles(dir, 0) // 0 = all
	for _, content := range allCmdFiles {
		if strings.Contains(content, "--stdin") && strings.Contains(content, "echo") {
			stdinExamples++
		}
	}
	if stdinExamples >= 3 {
		score++
	}
	// Token efficiency: --agent meta-flag
	if strings.Contains(rootContent, `"agent"`) && strings.Contains(rootContent, "PersistentPreRun") {
		score++
	}
	// Token efficiency: --compact strips verbose fields on single objects (blocklist approach)
	if strings.Contains(helpersContent, "compactObjectFields") || strings.Contains(helpersContent, "stripVerboseFields") {
		score++
	}
	// Token efficiency: analytics commands have --limit flag
	staleContent := readFileContent(filepath.Join(dir, "internal", "cli", "pm_stale.go"))
	loadContent := readFileContent(filepath.Join(dir, "internal", "cli", "pm_load.go"))
	if (strings.Contains(staleContent, `"limit"`) || staleContent == "") && (strings.Contains(loadContent, `"limit"`) || loadContent == "") {
		score++
	}
	// Token efficiency: store has ResolveByName for name-or-ID resolution
	storeContent := readFileContent(filepath.Join(dir, "internal", "store", "store.go"))
	if strings.Contains(storeContent, "ResolveByName") || strings.Contains(storeContent, "IsUUID") {
		score++
	}
	if score > 10 {
		score = 10
	}
	return score
}

func scoreMCPQuality(dir string) int {
	mcpContent := readFileContent(filepath.Join(dir, "internal", "mcp", "tools.go"))
	if mcpContent == "" {
		if !isLocalDatastoreCLIDir(dir) {
			return 0 // No MCP server generated
		}
		mcpContent = readAllGoFiles(filepath.Join(dir, "internal", "mcp"))
		if mcpContent == "" {
			return 0
		}
	}

	score := 0

	// Presence: MCP tools.go exists and has RegisterTools
	if strings.Contains(mcpContent, "RegisterTools") || strings.Contains(mcpContent, "NewServer") {
		score += 2
	}

	// Context tool: has rich context/about tool with domain knowledge
	if strings.Contains(mcpContent, `"context"`) || strings.Contains(mcpContent, "handleContext") || strings.Contains(mcpContent, "agent_context") {
		score += 2
	}

	// High-level tools: sql, search, sync exposed to MCP (not just CLI)
	highlevelCount := 0
	hasRuntimeMirror := strings.Contains(mcpContent, "cobratree.RegisterAll")
	if strings.Contains(mcpContent, `"sql"`) && strings.Contains(mcpContent, "handleSQL") {
		highlevelCount++
	}
	hasRegisteredSearch := hasRuntimeMirror && hasRegisteredCommandFileWithPrefix(filepath.Join(dir, "internal", "cli"), "search")
	if strings.Contains(mcpContent, `"search"`) && (strings.Contains(mcpContent, "handleSearch") || hasRegisteredSearch) {
		highlevelCount++
	}
	if (strings.Contains(mcpContent, `"sync"`) && strings.Contains(mcpContent, "handleSync")) ||
		(hasRuntimeMirror && hasRegisteredCommandFileWithPrefix(filepath.Join(dir, "internal", "cli"), "sync")) {
		highlevelCount++
	}
	if highlevelCount >= 2 {
		score += 2
	} else if highlevelCount >= 1 {
		score++
	}

	// Description quality: response shape hints (Returns array, Returns object)
	returnHints := strings.Count(mcpContent, "Returns array") + strings.Count(mcpContent, "Returns ")
	if returnHints >= 3 {
		score += 2
	} else if returnHints >= 1 {
		score++
	}

	// No empty descriptions
	emptyDescs := strings.Count(mcpContent, `Description("")`)
	if emptyDescs == 0 {
		score++
	}

	// Description richness: tool descriptions reference other tools or provide usage guidance
	if strings.Contains(mcpContent, "Requires sync") || strings.Contains(mcpContent, "Call this first") {
		score++
	}

	if score > 10 {
		score = 10
	}
	return score
}

// MCPDescMinLen and MCPDescMinWords are the agent-grade-description
// thresholds used by both `cli-printing-press tools-audit` and the
// scorecard's mcp_description_quality dimension. Defined here so a
// single edit keeps both surfaces in lockstep — internal/cli imports
// these for its thin-mcp-description check.
const (
	MCPDescMinLen   = 60
	MCPDescMinWords = 8
)

// IsThinMCPDescription reports whether a description trips the
// agent-grade floor: empty or both shorter than MCPDescMinLen AND
// fewer words than MCPDescMinWords. Shared between the audit (cli
// package) and the scorer so both apply identical semantics.
func IsThinMCPDescription(desc string) bool {
	d := strings.TrimSpace(desc)
	if d == "" {
		return true
	}
	return len(d) < MCPDescMinLen && len(strings.Fields(d)) < MCPDescMinWords
}

// ScoreMCPDescriptionQualityForManifest scores an already-parsed
// manifest. Callers that read tools-manifest.json for other reasons
// in the same code path (e.g., cli-printing-press tools-audit, which
// emits findings from the manifest) call this variant to avoid
// re-parsing.
func ScoreMCPDescriptionQualityForManifest(m *ToolsManifest) (score int, scored bool) {
	if m == nil || len(m.Tools) == 0 {
		return 0, false
	}
	if !m.EndpointMirrorsVisible() {
		return 0, false
	}
	thin := 0
	for _, t := range m.Tools {
		if IsThinMCPDescription(t.Description) {
			thin++
		}
	}
	pct := float64(thin) / float64(len(m.Tools))
	switch {
	case pct == 0:
		return 10, true
	case pct < 0.05:
		return 9, true
	case pct < 0.15:
		return 7, true
	case pct < 0.30:
		return 5, true
	case pct < 0.50:
		return 3, true
	default:
		return 0, true
	}
}

// scoreMCPDescriptionQuality measures how many of a CLI's typed MCP
// tools carry agent-grade descriptions vs. terse spec-derived
// summaries. Reads tools-manifest.json (the source of truth for typed
// endpoint tools' descriptions at runtime) and counts entries whose
// description trips IsThinMCPDescription — the same predicate
// `cli-printing-press tools-audit` uses for thin-mcp-description findings.
//
// Unscored when no manifest exists (legacy CLIs predating the
// manifest schema) or when the manifest has no tools.
//
// Score curve favors low thin-percentage steeply; the goal is to
// reward CLIs that have done the work to provide rich descriptions
// rather than to give partial credit to ones that haven't. CLIs whose
// descriptions are 50%+ thin score 0 — there's no signal to credit
// when most of the surface is unusable to an agent.
func scoreMCPDescriptionQuality(dir string) (score int, scored bool) {
	m, err := ReadToolsManifest(dir)
	if err != nil {
		return 0, false
	}
	return ScoreMCPDescriptionQualityForManifest(m)
}

func scoreLocalCache(dir string) int {
	if isLocalDatastoreCLIDir(dir) && hasLocalDatastoreCodeSignal(dir) {
		return 10
	}
	clientContent := readFileContent(filepath.Join(dir, "internal", "client", "client.go"))
	score := 0
	// Presence: GET response caching
	if strings.Contains(clientContent, "readCache") || strings.Contains(clientContent, "writeCache") || strings.Contains(clientContent, "cacheDir") {
		score += 2
	}
	// Presence: --no-cache bypass
	if strings.Contains(clientContent, "no-cache") || strings.Contains(clientContent, "NoCache") {
		score += 1
	}
	// Quality: cache has TTL (time-based expiry)
	if strings.Contains(clientContent, "time.Duration") || strings.Contains(clientContent, "ModTime") || strings.Contains(clientContent, "TTL") || strings.Contains(clientContent, "ttl") {
		score += 2
	}
	// Quality: XDG or standard cache directory
	if strings.Contains(clientContent, ".cache") || strings.Contains(clientContent, "XDG_CACHE_HOME") || strings.Contains(clientContent, "UserCacheDir") {
		score += 2
	}
	// Excellence: SQLite or embedded DB
	for _, name := range []string{"internal/cache/cache.go", "internal/store/store.go"} {
		content := readFileContent(filepath.Join(dir, name))
		if strings.Contains(content, "sqlite") || strings.Contains(content, "bolt") || strings.Contains(content, "badger") {
			score += 3
			break
		}
	}
	if score > 10 {
		score = 10
	}
	return score
}

// scoreCacheFreshness rewards the discrawl-inspired Phase 1-3 capabilities.
// Returns (score, true) when the CLI has a local store; (0, false) otherwise
// so the dimension is marked N/A and excluded from the tier1 denominator.
// Each sub-capability is worth 2-5 points; the total is capped at 10.
//
// The signals are string-matched against well-known template artifacts
// rather than imported symbols because the scorer must not depend on
// the generated package paths — every printed CLI has a distinct module
// path.
func scoreCacheFreshness(dir string) (int, bool) {
	storePath := filepath.Join(dir, "internal", "store", "store.go")
	storeContent := readFileContent(storePath)
	if storeContent == "" {
		return 0, false
	}
	score := 0

	// (a) Schema-version gate: the StoreSchemaVersion constant + PRAGMA
	// user_version read/write fail-fast against schema drift. Worth 3 pts
	// because without it, binary upgrades silently corrupt reads.
	if strings.Contains(storeContent, "StoreSchemaVersion") && strings.Contains(storeContent, "user_version") {
		score += 3
	}

	// (b) Doctor cache section: collectCacheReport + renderCacheReport
	// emit the agent-consumable freshness surface.
	doctorPath := filepath.Join(dir, "internal", "cli", "doctor.go")
	doctorContent := readFileContent(doctorPath)
	if strings.Contains(doctorContent, "collectCacheReport") {
		score += 2
	}

	// (c) Auto-refresh wired: the PersistentPreRunE hook invokes
	// autoRefreshIfStale, and the cliutil freshness helper exists. Worth
	// 5 pts because this is the user's headline ask — a CLI that ships
	// schema gating + doctor cache + auto-refresh has perfect freshness
	// behavior and should top the dimension out at 10/10.
	autoRefreshPath := filepath.Join(dir, "internal", "cli", "auto_refresh.go")
	autoRefreshContent := readFileContent(autoRefreshPath)
	freshnessPath := filepath.Join(dir, "internal", "cliutil", "freshness.go")
	freshnessContent := readFileContent(freshnessPath)
	if strings.Contains(autoRefreshContent, "autoRefreshIfStale") && strings.Contains(freshnessContent, "EnsureFresh") {
		score += 5
	} else if hasQuotaAwareFreshnessDesign(dir, storeContent) {
		// Quota-aware CLIs deliberately omit auto-refresh, so rescale the
		// remaining 5-point subtotal onto the full 10-point dimension.
		score *= 2
	}

	if score > 10 {
		score = 10
	}
	return score, true
}

func hasQuotaAwareFreshnessDesign(dir, storeContent string) bool {
	if strings.Contains(storeContent, "lookup_log") {
		return true
	}
	quotaContent := readFileContent(filepath.Join(dir, "internal", "cliutil", "quota.go"))
	if quotaContent == "" || !strings.Contains(strings.ToLower(quotaContent), "quota") {
		return false
	}
	return strings.Contains(quotaContent, "Daily") ||
		strings.Contains(quotaContent, "PerDay") ||
		quotaDailySignalRE.MatchString(quotaContent)
}

// scoreLiveAPIVerification returns a 0-10 score reflecting whether verify
// ran against the real API and how many of its checks passed. It returns
// (0, false) in every case where the signal is absent or untrustworthy:
// no verify report threaded in, verify ran against a mock, or verify ran
// in structural-only mode. A scored result always means the CLI has at
// least been exercised against its real backend.
//
// PassRate is already 0-100 (e.g., 91.0 for 91%), matching the existing
// calibration path in RunScorecard. The 95% cap at 10 mirrors the
// established convention that near-perfect is perfect for grading.
func scoreLiveAPIVerification(verifyReport *VerifyReport) (int, bool) {
	if verifyReport == nil {
		return 0, false
	}
	if verifyReport.Mode != "live" {
		return 0, false
	}
	if verifyReport.PassRate >= 95 {
		return 10, true
	}
	// Linear scale below the cap: 0% → 0, 10% → 1, ..., 94% → 9.
	score := min(max(int(verifyReport.PassRate/10), 0), 10)
	return score, true
}

// ApplyLiveCheckToScorecard lets sampled command output affect only the
// scorecard dimensions it can honestly support. A weak or failing sample can
// cap Insight, but it must not populate LiveAPIVerification; that dimension
// is reserved for VerifyReport evidence from real live verify runs.
func ApplyLiveCheckToScorecard(sc *Scorecard, live *LiveCheckResult) {
	if sc == nil {
		return
	}
	insightCap := InsightCapFromLiveCheck(live)
	if insightCap == nil || sc.Steinberger.Insight <= *insightCap {
		return
	}
	sc.Steinberger.Insight = *insightCap
	recomputeScorecardTotals(sc)
	applyScorecardCalibration(sc)
	sc.OverallGrade = computeGrade(sc.Steinberger.Percentage)
	sc.GapReport = buildGapReport(sc.Steinberger, sc.UnscoredDimensions)
	appendNovelFeatureDepthGaps(sc)
}

func scorecardNovelFeatureDepthMismatches(outputDir, pipelineDir string) []NovelFeatureDepthMismatch {
	if pipelineDir == "" {
		return nil
	}
	research, err := LoadResearch(pipelineDir)
	if err != nil || len(research.NovelFeatures) == 0 {
		return nil
	}
	paths, leaves := collectRegisteredCommands(outputDir)
	var mismatches []NovelFeatureDepthMismatch
	for _, nf := range research.NovelFeatures {
		if !matchNovelFeature(nf, paths, leaves) {
			continue
		}
		if mismatch := novelFeatureDepthMismatch(nf, paths); mismatch != nil {
			mismatches = append(mismatches, *mismatch)
		}
	}
	return mismatches
}

func appendNovelFeatureDepthGaps(sc *Scorecard) {
	for _, mismatch := range sc.NovelFeatureDepthMismatches {
		sc.GapReport = append(sc.GapReport, fmt.Sprintf(
			"novel feature command-depth mismatch: %s advertised as %s but registered as %s",
			mismatch.Command, mismatch.Advertised, mismatch.Actual))
	}
}

func applyScorecardCalibration(sc *Scorecard) {
	if sc == nil {
		return
	}
	var notes []string
	if sc.verifyCalibrationFloor > 0 && sc.Steinberger.Total < sc.verifyCalibrationFloor {
		originalTotal := sc.Steinberger.Total
		sc.Steinberger.Total = sc.verifyCalibrationFloor
		notes = append(notes, fmt.Sprintf(
			"Score raised from %d to %d based on verify pass rate",
			originalTotal, sc.verifyCalibrationFloor))
	}
	if sc.browserSessionUnverified && sc.Steinberger.Total > 69 {
		sc.Steinberger.Total = 69
		notes = append(notes, "Score capped because required browser-session auth was not verified")
	}
	sc.Steinberger.Percentage = sc.Steinberger.Total
	sc.Steinberger.CalibrationNote = strings.Join(notes, "; ")
}

func recomputeScorecardTotals(sc *Scorecard) {
	tier1Raw := sumScorecardDimensions(
		sc.Steinberger.OutputModes,
		sc.Steinberger.Auth,
		sc.Steinberger.ErrorHandling,
		sc.Steinberger.TerminalUX,
		sc.Steinberger.README,
		sc.Steinberger.Doctor,
		sc.Steinberger.AgentNative,
		sc.Steinberger.MCPQuality,
		sc.Steinberger.MCPDescriptionQuality,
		sc.Steinberger.MCPTokenEff,
		sc.Steinberger.MCPRemoteTransport,
		sc.Steinberger.MCPToolDesign,
		sc.Steinberger.MCPSurfaceStrategy,
		sc.Steinberger.LocalCache,
		sc.Steinberger.CacheFreshness,
		sc.Steinberger.Breadth,
		sc.Steinberger.Vision,
		sc.Steinberger.Workflows,
		sc.Steinberger.Insight,
		sc.Steinberger.AgentWorkflow,
	)

	tier1Max := scorecardTierMax(sc, 200, DimMCPDescriptionQuality, DimMCPTokenEfficiency, DimCacheFreshness, DimMCPRemoteTransport, DimMCPToolDesign, DimMCPSurfaceStrategy, DimLocalCache, DimVision, DimWorkflows, DimInsight, DimAgentWorkflow)
	tier1Normalized := 0
	if tier1Max > 0 {
		tier1Normalized = (tier1Raw * 50) / tier1Max
	}

	tier2Raw := sumScorecardDimensions(
		sc.Steinberger.PathValidity,
		sc.Steinberger.AuthProtocol,
		sc.Steinberger.DataPipelineIntegrity,
		sc.Steinberger.SyncCorrectness,
		sc.Steinberger.TypeFidelity,
		sc.Steinberger.DeadCode,
		sc.Steinberger.LiveAPIVerification,
	)

	tier2Max := scorecardTierMax(sc, 60, DimLiveAPIVerification, DimPathValidity, DimAuthProtocol, DimSyncCorrectness, DimDataPipelineIntegrity, DimTypeFidelity)
	tier2Normalized := 0
	if tier2Max > 0 {
		tier2Normalized = (tier2Raw * 50) / tier2Max
	}
	sc.Steinberger.Total = tier1Normalized + tier2Normalized
	sc.Steinberger.Percentage = sc.Steinberger.Total
}

func sumScorecardDimensions(scores ...int) (total int) {
	for _, score := range scores {
		total += score
	}
	return total
}

func scorecardTierMax(sc *Scorecard, base int, optionalDimensions ...string) int {
	max := base
	for _, name := range optionalDimensions {
		if sc.IsDimensionUnscored(name) {
			max -= scorecardDimensionMax(name)
		}
	}
	return max
}

func scorecardDimensionMax(name string) int {
	if name == DimTypeFidelity || name == DimDeadCode {
		return 5
	}
	return 10
}

func hasScorecardLocalStore(dir string) bool {
	return fileExists(filepath.Join(dir, "internal", "store", "store.go"))
}

func scoreBreadth(dir string) int {
	cliDir := filepath.Join(dir, "internal", "cli")
	entries, err := os.ReadDir(cliDir)
	if err != nil {
		return 0
	}
	commandFiles := 0
	lazyDescs := 0
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
			continue
		}
		if infraAllFiles[e.Name()] {
			continue
		}
		commandFiles++
		// Check for lazy 1-word descriptions
		content := readFileContent(filepath.Join(cliDir, e.Name()))
		if hasLazyDescription(content) {
			lazyDescs++
		}
	}

	var score int
	switch {
	case commandFiles >= 60:
		score = 8
	case commandFiles >= 41:
		score = 7
	case commandFiles >= 21:
		score = 5
	case commandFiles >= 11:
		score = 4
	case commandFiles >= 5:
		score = 2
	default:
		return 0
	}
	// Penalty: if more than 50% of commands have lazy 1-word descriptions
	if commandFiles > 0 && lazyDescs*2 > commandFiles {
		score -= 2
	}
	// Bonus: if descriptions are mostly quality (< 20% lazy)
	if commandFiles > 0 && lazyDescs*5 < commandFiles {
		score += 2
	}
	if score > 10 {
		score = 10
	}
	if score < 0 {
		score = 0
	}
	return score
}

func scoreVision(dir string) int {
	cliDir := filepath.Join(dir, "internal", "cli")
	registeredFiles := registeredCommandFiles(cliDir)
	commandContent := registeredCommandContent(cliDir, registeredFiles)

	// Tier 1: Feature Presence (0-5 points)
	tier1 := 0.0
	if fileExists(filepath.Join(cliDir, "export.go")) {
		tier1 += 1.0
	} else if hasCommandContentMatching(commandContent, isVisionExportShape) {
		tier1 += 1.0
	}
	if fileExists(filepath.Join(dir, "internal", "store", "store.go")) {
		tier1 += 1.0
	}
	if fileExists(filepath.Join(cliDir, "search.go")) {
		tier1 += 1.0
	}
	if hasRegisteredCommandFileWithPrefix(cliDir, "sync") {
		tier1 += 0.5
	}
	if fileExists(filepath.Join(cliDir, "tail.go")) {
		tier1 += 0.5
	}
	if fileExists(filepath.Join(cliDir, "import.go")) {
		tier1 += 0.5
	}
	// internal/learn/doc.go is the presence sentinel for the recall/teach loop (+0.5).
	if fileExists(filepath.Join(dir, "internal", "learn", "doc.go")) {
		tier1 += 0.5
	}
	// Workflow or compound command files
	hasWorkflowShape := false
	for name := range commandContent {
		if strings.Contains(name, "_workflow") || strings.Contains(name, "_compound") {
			if strings.HasSuffix(name, ".go") {
				hasWorkflowShape = true
				break
			}
		}
	}
	if hasWorkflowShape || hasCommandContentMatching(commandContent, isVisionWorkflowShape) {
		tier1 += 0.5
	}
	if tier1 > 5 {
		tier1 = 5
	}

	// Tier 2: Feature Intelligence (0-5 points)
	// - schema+wiring combined cap: 3.5
	// - FTS5: 1.0
	// - search uses store: 0.5
	tier2 := 0.0

	// Schema depth (0-1.5): check if store.go has domain-specific tables
	storePath := filepath.Join(dir, "internal", "store", "store.go")
	if fileExists(storePath) {
		storeContent := readFileContent(storePath)
		tableCount := strings.Count(storeContent, "CREATE TABLE")
		syncStateCount := strings.Count(storeContent, "sync_state")
		domainTables := tableCount
		if syncStateCount > 0 {
			domainTables-- // Don't count sync_state as a domain table
		}
		if domainTables >= 3 {
			tier2 += 1.5
		} else if domainTables >= 2 {
			tier2 += 1.0
		} else if domainTables >= 1 {
			tier2 += 0.5
		}
	}

	// Wiring check (0-1.5): are vision commands registered in root.go?
	rootPath := filepath.Join(cliDir, "root.go")
	if fileExists(rootPath) {
		rootContent := readFileContent(rootPath)
		visionFuncs := []string{"newSyncCmd", "newSearchCmd", "newExportCmd", "newTailCmd", "newImportCmd", "newAnalyticsCmd"}
		wired := 0
		for _, fn := range visionFuncs {
			if strings.Contains(rootContent, fn) {
				wired++
			}
		}
		tier2 += float64(wired) * 0.25
		tier2 += float64(registeredVisionCapabilityFiles(commandContent)) * 0.75
		if tier2 > 3.5 { // cap schema+wiring contribution so tier2 can reach its documented 5.0 max
			tier2 = 3.5
		}
	}

	// FTS5 check (0-1.0): does the store have full-text search?
	if fileExists(storePath) {
		storeContent := readFileContent(storePath)
		if strings.Contains(storeContent, "fts5") || strings.Contains(storeContent, "FTS5") {
			tier2 += 1.0
		}
	}

	// Search uses store (0-0.5): does search.go reference the store package?
	searchPath := filepath.Join(cliDir, "search.go")
	if fileExists(searchPath) {
		searchContent := readFileContent(searchPath)
		if strings.Contains(searchContent, "store.") || strings.Contains(searchContent, "/store") {
			tier2 += 0.5
		}
	}

	if tier2 > 5 {
		tier2 = 5
	}

	score := min(int(tier1+tier2), 10)
	return score
}

func registeredCommandContent(cliDir string, registeredFiles map[string]bool) map[string]string {
	entries, err := os.ReadDir(cliDir)
	if err != nil {
		return nil
	}
	contentByName := map[string]string{}
	for _, entry := range entries {
		name := entry.Name()
		if entry.IsDir() || !strings.HasSuffix(name, ".go") || infraCoreFiles[name] {
			continue
		}
		if len(registeredFiles) > 0 && !registeredFiles[name] {
			continue
		}
		contentByName[name] = readFileContent(filepath.Join(cliDir, name))
	}
	return contentByName
}

func hasCommandContentMatching(commandContent map[string]string, match func(string) bool) bool {
	for _, content := range commandContent {
		if match(content) {
			return true
		}
	}
	return false
}

func registeredVisionCapabilityFiles(commandContent map[string]string) int {
	count := 0
	for _, content := range commandContent {
		if isVisionExportShape(content) || isVisionWorkflowShape(content) {
			count++
		}
	}
	return count
}

func isVisionExportShape(content string) bool {
	hasDataSource := hasStoreSignal(content) || hasGenericResourcesSQLSearchSignal(content)
	if !hasDataSource {
		return false
	}
	return strings.Contains(content, "json.NewEncoder") ||
		strings.Contains(content, "csv.NewWriter")
}

func isVisionWorkflowShape(content string) bool {
	return countClientAPICalls(content) >= 2
}

func countClientAPICalls(content string) int {
	return len(clientAPICallRE.FindAllString(content, -1))
}

func clientHelperCallCounts(fileContent map[string]string) map[string]int {
	helpers := map[string]int{}
	for fileName, content := range fileContent {
		fset := token.NewFileSet()
		file, err := parser.ParseFile(fset, "", content, 0)
		if err != nil {
			continue
		}
		imports := clientImportAliases(file)
		for _, decl := range file.Decls {
			fn, ok := decl.(*ast.FuncDecl)
			if !ok || fn.Name == nil || fn.Recv != nil || fn.Body == nil {
				continue
			}
			start := fset.Position(fn.Body.Pos()).Offset
			end := fset.Position(fn.Body.End()).Offset
			if start < 0 || end > len(content) || start >= end {
				continue
			}
			body := content[start:end]
			calls := countClientAPICalls(body) + countImportedClientCalls(fn.Body, imports, false)
			if calls > 0 {
				helpers[helperKey(fileName, fn.Name.Name)] = calls
			}
		}
	}
	return helpers
}

func helperKey(fileName, funcName string) string {
	return fileName + ":" + funcName
}

func splitHelperKey(key string) (string, string) {
	i := strings.LastIndexByte(key, ':')
	if i < 0 {
		return "", key
	}
	return key[:i], key[i+1:]
}

func countImportedClientCalls(body *ast.BlockStmt, imports map[string]clientImportKind, allowAnySiblingSelector bool) int {
	if body == nil {
		return 0
	}
	count := 0
	ast.Inspect(body, func(n ast.Node) bool {
		call, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}
		if isImportedClientCall(call.Fun, imports, allowAnySiblingSelector) {
			count++
		}
		return true
	})
	return count
}

// cobraUseLeafRe extracts the leaf command name from a Cobra Use: literal.
// Accepts both Go string forms — double-quoted and backtick raw-string —
// because authors reach for backticks when the value contains a literal
// double-quote.
var cobraUseLeafRe = regexp.MustCompile("Use:\\s*[\"`]([^\"`\\s]+)")

// manifestNovelFeatureLeaves returns the leaves of every novel_features[].command
// in dir/.printing-press.json. Returns nil when the manifest is missing,
// unparseable, or carries no novel features.
func manifestNovelFeatureLeaves(dir string) map[string]bool {
	data, err := os.ReadFile(filepath.Join(dir, CLIManifestFilename))
	if err != nil {
		return nil
	}
	var m CLIManifest
	if err := json.Unmarshal(data, &m); err != nil {
		return nil
	}
	if len(m.NovelFeatures) == 0 {
		return nil
	}
	leaves := make(map[string]bool, len(m.NovelFeatures))
	for _, nf := range m.NovelFeatures {
		_, leaf := splitCommandPath(commandPath(nf.Command))
		if leaf != "" {
			leaves[leaf] = true
		}
	}
	return leaves
}

// registeredCommandFiles returns the set of cli/*.go filenames whose command
// constructor is wired into the Cobra tree through AddCommand calls. Files
// without a registered constructor should not inflate workflow/insight scores
// even if they match prefix or behavioral heuristics — they're orphans, dead
// code, or half-built commands that the user cannot actually invoke.
//
// Returns an empty map if root.go is missing or parsing yields no matches so
// callers can fall open to the prior heuristic behavior (older or partial CLI
// trees where the registration graph isn't parseable).
func registeredCommandFiles(cliDir string) map[string]bool {
	rootContent := readFileContent(filepath.Join(cliDir, "root.go"))
	if rootContent == "" {
		return map[string]bool{}
	}

	reachableCtors := addCommandConstructorCalls(rootContent)
	if len(reachableCtors) == 0 {
		return map[string]bool{}
	}

	// Walk cli/*.go and map each file to the reachable constructor it defines.
	// Re-scan newly reachable command files for child AddCommand calls so
	// subcommands defined outside root.go still count as user-reachable.
	defRe := regexp.MustCompile(`^func\s+(new[A-Z][A-Za-z0-9_]*Cmd)\s*\(`)
	entries, err := os.ReadDir(cliDir)
	if err != nil {
		return map[string]bool{}
	}
	fileContent := map[string]string{}
	fileDefs := map[string][]string{}
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
			continue
		}
		content := readFileContent(filepath.Join(cliDir, e.Name()))
		fileContent[e.Name()] = content
		for line := range strings.SplitSeq(content, "\n") {
			sm := defRe.FindStringSubmatch(line)
			if sm != nil {
				fileDefs[e.Name()] = append(fileDefs[e.Name()], sm[1])
			}
		}
	}

	result := make(map[string]bool)
	for {
		changed := false
		for name, defs := range fileDefs {
			reachable := false
			for _, def := range defs {
				if reachableCtors[def] {
					reachable = true
					break
				}
			}
			if !reachable || result[name] {
				continue
			}

			result[name] = true
			changed = true
			for ctor := range addCommandConstructorCalls(fileContent[name]) {
				reachableCtors[ctor] = true
			}
		}
		if !changed {
			break
		}
	}
	return result
}

func addCommandConstructorCalls(content string) map[string]bool {
	file, err := parser.ParseFile(token.NewFileSet(), "", content, 0)
	if err != nil {
		return map[string]bool{}
	}

	ctors := map[string]bool{}
	ast.Inspect(file, func(n ast.Node) bool {
		call, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}
		sel, ok := call.Fun.(*ast.SelectorExpr)
		if !ok || sel.Sel == nil || sel.Sel.Name != "AddCommand" {
			return true
		}
		for _, arg := range call.Args {
			if ctor := commandConstructorName(arg); ctor != "" {
				ctors[ctor] = true
			}
		}
		return true
	})
	return ctors
}

func commandConstructorName(expr ast.Expr) string {
	call, ok := expr.(*ast.CallExpr)
	if !ok {
		return ""
	}
	ident, ok := call.Fun.(*ast.Ident)
	if !ok || !isCommandConstructorName(ident.Name) {
		return ""
	}
	return ident.Name
}

func isCommandConstructorName(name string) bool {
	if !strings.HasPrefix(name, "new") || !strings.HasSuffix(name, "Cmd") {
		return false
	}
	stem := strings.TrimSuffix(strings.TrimPrefix(name, "new"), "Cmd")
	return stem != "" && stem[0] >= 'A' && stem[0] <= 'Z'
}

func scoreWorkflows(dir string) int {
	cliDir := filepath.Join(dir, "internal", "cli")
	entries, err := os.ReadDir(cliDir)
	if err != nil {
		return 0
	}

	// Build the set of files whose command constructor is actually registered in
	// root.go. Files that define constructors never added to the command tree —
	// whether orphaned, dead, or pending — should not inflate the score. This
	// also prevents dead-code removal from dropping the score: a file whose
	// constructor isn't registered isn't counted in the first place.
	registeredFiles := registeredCommandFiles(cliDir)
	fileContent := map[string]string{}
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") {
			continue
		}
		fileContent[e.Name()] = readFileContent(filepath.Join(cliDir, e.Name()))
	}
	storeHelpers := storeHelperNames(fileContent)
	clientHelpers := clientHelperCallCounts(fileContent)

	// Some prefixes overlap with insightPrefixes intentionally — per Steinberger,
	// analytics/insights ARE compound commands (the visionary research plan lists
	// "analytics" alongside "backup" and "moderate" as workflow examples). A command
	// like stats.go correctly scores in both dimensions.
	workflowPrefixes := []string{"stale", "orphan", "triage", "load", "overdue", "standup", "deps", "workflow",
		"agenda", "free", "conflicts", "unconfirmed", "stats", "trends", "health",
		"reconcile", "revenue", "archive", "search", "sync", "busy", "export",
		"noshow", "reassign", "clone"}

	compoundCommands := 0
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
			continue
		}
		if infraCoreFiles[e.Name()] {
			continue
		}

		// If root.go registration is discoverable, require the file to define a
		// registered constructor. Falls open when no registrations are found at
		// all (older CLIs or partial builds) so we don't zero out scores
		// unexpectedly.
		if len(registeredFiles) > 0 && !registeredFiles[e.Name()] {
			continue
		}

		name := strings.ToLower(e.Name())

		// Detect workflow commands by filename pattern
		isWorkflowFile := false
		for _, prefix := range workflowPrefixes {
			if strings.HasPrefix(name, prefix) {
				isWorkflowFile = true
				break
			}
		}
		if isWorkflowFile {
			compoundCommands++
			continue
		}

		content := fileContent[e.Name()]

		// A command that uses the local data layer is a workflow command.
		if hasStoreSignal(content) || callsStoreHelper(content, storeHelpers) {
			compoundCommands++
			continue
		}

		// Count files that make 2+ API calls (total occurrences, not unique methods).
		// A command calling c.Get 3 times is a compound workflow even if it never uses POST.
		apiCalls := countClientAPICalls(content) + countWeightedHelperCallsFiltered(content, clientHelpers, func(fileName, name string) bool {
			return fileName != e.Name()
		})
		if strings.Contains(content, "store.") {
			apiCalls++
		}
		if apiCalls >= 2 {
			compoundCommands++
		}
	}

	switch {
	case compoundCommands >= 7:
		return 10
	case compoundCommands >= 5:
		return 8
	case compoundCommands >= 3:
		return 6
	case compoundCommands >= 2:
		return 4
	case compoundCommands >= 1:
		return 2
	default:
		return 0
	}
}

func scoreInsight(dir string) int {
	cliDir := filepath.Join(dir, "internal", "cli")
	entries, err := os.ReadDir(cliDir)
	if err != nil {
		return 0
	}

	registeredFiles := registeredCommandFiles(cliDir)

	insightPrefixes := []string{"health", "similar", "bottleneck", "trends", "patterns", "forecast",
		"stats", "conflicts", "stale", "analytics", "busiest", "velocity",
		"utilization", "coverage", "gaps", "noshow"}

	// novelLeaves credits files whose Cobra Use matches an agent-declared
	// transcendence command. The prefix list above can't enumerate every
	// reasonable insight verb across APIs.
	novelLeaves := manifestNovelFeatureLeaves(dir)

	found := 0
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
			continue
		}
		if infraCoreFiles[e.Name()] {
			continue
		}
		if len(registeredFiles) > 0 && !registeredFiles[e.Name()] {
			continue
		}
		name := strings.ToLower(e.Name())

		// Signal 1: filename prefix match
		prefixMatch := false
		for _, prefix := range insightPrefixes {
			if strings.HasPrefix(name, prefix) {
				prefixMatch = true
				break
			}
		}
		if prefixMatch {
			found++
			continue
		}

		content := readFileContent(filepath.Join(cliDir, e.Name()))
		if content == "" {
			continue
		}

		// Signal 2: file declares a Cobra Use: matching an agent-declared novel feature.
		if len(novelLeaves) > 0 {
			matchedNovelLeaf := false
			for _, m := range cobraUseLeafRe.FindAllStringSubmatch(content, -1) {
				if novelLeaves[strings.ToLower(m[1])] {
					matchedNovelLeaf = true
					break
				}
			}
			if matchedNovelLeaf {
				found++
				continue
			}
		}

		// Signal 3: store + SQL aggregation
		usesStore := hasStoreSignal(content)
		rateRe := regexp.MustCompile(`\brate\b|\bRate\b`)
		hasSQLAgg := strings.Contains(content, "COUNT(") || strings.Contains(content, "SUM(") ||
			strings.Contains(content, "GROUP BY") || strings.Contains(content, "AVG(") ||
			rateRe.MatchString(content)
		if usesStore && hasSQLAgg {
			found++
			continue
		}

		// Signal 4: behavioral — command produces derived/aggregated output.
		// Detects Go-level aggregation: sorting, percentage calculations, comparisons,
		// summary statistics. Requires multi-source input (2+ API calls or store usage)
		// to avoid counting simple pass-through commands.
		apiCallCount := countClientAPICalls(content)
		hasMultiSource := apiCallCount >= 2 || usesStore

		hasGoAgg := strings.Contains(content, "sort.Slice") ||
			strings.Contains(content, "sort.Sort") ||
			strings.Contains(content, "* 100") ||
			strings.Contains(content, "/ total") ||
			strings.Contains(content, "/ float64(") ||
			strings.Contains(content, `fmt.Sprintf("%.`) ||
			strings.Contains(content, "percentage") ||
			strings.Contains(content, "Percentage") ||
			strings.Contains(content, "completion") ||
			strings.Contains(content, "Completion")

		if hasMultiSource && hasGoAgg {
			found++
		}
	}

	switch {
	case found >= 6:
		return 10
	case found >= 5:
		return 9
	case found >= 4:
		return 8
	case found >= 3:
		return 6
	case found >= 2:
		return 4
	case found >= 1:
		return 2
	default:
		return 0
	}
}

// scoreAgentWorkflow measures how well the generated CLI supports the
// HeyGen-derived agent-workflow pattern: async jobs with --wait, named
// profiles, routed delivery, and an in-band feedback channel. Each
// capability is worth 2-3 points; a fully-equipped CLI scores 10.
//
// Profiles, deliver, and feedback are always applicable. The async
// sub-score is only earned when the generator detected at least one
// async endpoint (jobs.go is emitted); for purely synchronous specs
// the missing async capability is not a penalty because there is
// nothing to submit-then-poll.
func scoreAgentWorkflow(dir string) int {
	cliDir := filepath.Join(dir, "internal", "cli")

	exists := func(name string) bool {
		_, err := os.Stat(filepath.Join(cliDir, name))
		return err == nil
	}

	hasJobs := exists("jobs.go")
	hasProfile := exists("profile.go")
	hasDeliver := exists("deliver.go")
	hasFeedback := exists("feedback.go")

	score := 0
	if hasProfile {
		score += 3
	}
	if hasDeliver {
		score += 3
	}
	if hasFeedback {
		score += 2
	}
	if hasJobs {
		score += 2
	} else {
		// Async is spec-driven. Reward 1 for not needing it when the
		// other three agent-workflow capabilities are present - the CLI
		// is correctly shaped for its synchronous API.
		if hasProfile && hasDeliver && hasFeedback {
			score += 1
		}
	}
	if score > 10 {
		score = 10
	}
	return score
}

type openAPISecurityScheme struct {
	Key        string
	Type       string
	Scheme     string
	In         string
	HeaderName string
	EnvVars    []string
	// Prefix mirrors apispec.AuthConfig.Prefix for internal-YAML specs that
	// override the literal "Bearer" scheme word (e.g., "Token", "PRIVATE-TOKEN").
	// Empty for OpenAPI-derived schemes; bearer-branch scoring falls back to "Bearer".
	Prefix string
}

const (
	composedHeaderSuffixSignature = "SIGNATURE"
	composedHeaderSuffixTimestamp = "TIMESTAMP"
	composedHeaderSuffixDate      = "DATE"
	composedHeaderSuffixNonce     = "NONCE"
	composedHeaderSuffixDigest    = "DIGEST"
)

type securityRequirementSet struct {
	Alternatives    [][]string
	AllowsAnonymous bool
}

type oauthScopeRequirement struct {
	Endpoint     string                  `json:"endpoint"`
	OperationID  string                  `json:"operation_id,omitempty"`
	Alternatives []oauthScopeAlternative `json:"alternatives"`
}

type oauthScopeAlternative struct {
	Scopes []string `json:"scopes"`
}

type openAPISpecInfo struct {
	Paths                  []string
	SecuritySchemes        map[string]openAPISecurityScheme
	SecurityRequirements   []securityRequirementSet
	OAuthScopeRequirements []oauthScopeRequirement
	PositionalParamCount   int
	Kind                   string // see apispec.KindREST / apispec.KindSynthetic
	IsGraphQL              bool
}

func (s *openAPISpecInfo) IsSynthetic() bool {
	return s != nil && s.Kind == apispec.KindSynthetic
}

func loadOpenAPISpec(specPath string) (*openAPISpecInfo, error) {
	if specPath == "" {
		return nil, nil
	}

	data, err := openapi.LoadSpecBytes(specPath, false, false)
	if err != nil {
		return nil, fmt.Errorf("reading spec: %w", err)
	}
	return loadOpenAPISpecData(data, specPath)
}

func loadOpenAPISpecData(data []byte, specPath string) (*openAPISpecInfo, error) {
	// Detect internal YAML spec format and convert to openAPISpecInfo.
	if isInternalYAMLSpec(data) {
		internal, err := apispec.ParseBytes(data)
		if err != nil {
			return nil, fmt.Errorf("parsing internal YAML spec: %w", err)
		}
		return internalSpecToOpenAPISpecInfo(internal), nil
	}

	// Strip a UTF-8 BOM if present so editors that emit one (Windows
	// Notepad, some VS Code locales) don't fail content sniffing or
	// json.Unmarshal, which both reject leading BOM bytes.
	data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})

	// Distinguish OpenAPI JSON from OpenAPI YAML by content sniffing the
	// first non-whitespace byte. JSON objects start with `{`; YAML mappings
	// start with letters/quotes. yaml.Unmarshal would accept either format,
	// but separate branches preserve format-specific error messages.
	var raw map[string]any
	trimmed := bytes.TrimLeft(data, " \t\r\n")
	if len(trimmed) == 0 {
		return nil, fmt.Errorf("spec file %s is empty", specPath)
	}
	if trimmed[0] == '{' {
		if err := json.Unmarshal(data, &raw); err != nil {
			return nil, fmt.Errorf("parsing spec JSON: %w", err)
		}
	} else {
		if err := yaml.Unmarshal(data, &raw); err != nil {
			return nil, fmt.Errorf("parsing OpenAPI YAML spec: %w", err)
		}
	}

	info := &openAPISpecInfo{
		SecuritySchemes: make(map[string]openAPISecurityScheme),
		IsGraphQL:       hasGraphQLEndpointExtension(raw),
	}
	if paths, ok := raw["paths"].(map[string]any); ok {
		for path := range paths {
			info.Paths = append(info.Paths, path)
			info.PositionalParamCount += countPathTemplateParams(path)
		}
		slices.Sort(info.Paths)
	}

	if components, ok := raw["components"].(map[string]any); ok {
		if securitySchemes, ok := components["securitySchemes"].(map[string]any); ok {
			for schemeName, value := range securitySchemes {
				scheme := openAPISecurityScheme{Key: schemeName}
				if fields, ok := value.(map[string]any); ok {
					scheme.Type = strings.ToLower(asString(fields["type"]))
					scheme.Scheme = strings.ToLower(asString(fields["scheme"]))
					scheme.In = strings.ToLower(asString(fields["in"]))
					scheme.HeaderName = asString(fields["name"])
					scheme.EnvVars = parseScorecardAuthEnvVars(fields["x-auth-env-vars"])
				}
				info.SecuritySchemes[schemeName] = scheme
			}
		}
	}

	// Swagger 2.0 fallback: check top-level securityDefinitions when OAS3
	// components.securitySchemes is empty or missing.
	if len(info.SecuritySchemes) == 0 {
		if securityDefs, ok := raw["securityDefinitions"].(map[string]any); ok {
			for schemeName, value := range securityDefs {
				scheme := openAPISecurityScheme{Key: schemeName}
				if fields, ok := value.(map[string]any); ok {
					swType := strings.ToLower(asString(fields["type"]))
					swIn := strings.ToLower(asString(fields["in"]))
					swName := asString(fields["name"])

					switch swType {
					case "oauth2":
						scheme.Type = "oauth2"
					case "apikey":
						scheme.Type = "apikey"
						scheme.In = swIn
						scheme.HeaderName = swName
						scheme.EnvVars = parseScorecardAuthEnvVars(fields["x-auth-env-vars"])
						// Detect Bearer-style API key in header with Authorization name.
						if swIn == "header" && strings.EqualFold(swName, "Authorization") {
							scheme.Type = "http"
							scheme.Scheme = "bearer"
						}
					case "basic":
						scheme.Type = "http"
						scheme.Scheme = "basic"
					}
				}
				info.SecuritySchemes[schemeName] = scheme
			}
		}
	}

	rootSecurity, rootHasSecurity := parseSecurityRequirementSet(raw["security"])
	rootOAuthScopes, rootHasOAuthScopes := parseOAuthScopeAlternatives(raw["security"], info.SecuritySchemes)
	foundOperation := false
	if paths, ok := raw["paths"].(map[string]any); ok {
		pathNames := make([]string, 0, len(paths))
		for path := range paths {
			pathNames = append(pathNames, path)
		}
		slices.Sort(pathNames)
		for _, path := range pathNames {
			pathValue := paths[path]
			pathItem, ok := pathValue.(map[string]any)
			if !ok {
				continue
			}
			methods := make([]string, 0, len(pathItem))
			for method := range pathItem {
				if !isHTTPMethod(method) {
					continue
				}
				methods = append(methods, method)
			}
			slices.Sort(methods)
			for _, method := range methods {
				operationValue := pathItem[method]
				foundOperation = true
				operation, ok := operationValue.(map[string]any)
				if !ok {
					continue
				}
				if requirementSet, ok := parseSecurityRequirementSet(operation["security"]); ok {
					info.SecurityRequirements = append(info.SecurityRequirements, requirementSet)
					if alternatives, ok := parseOAuthScopeAlternatives(operation["security"], info.SecuritySchemes); ok && len(alternatives) > 0 {
						info.OAuthScopeRequirements = append(info.OAuthScopeRequirements, oauthScopeRequirement{
							Endpoint:     strings.ToUpper(method) + " " + path,
							OperationID:  operationIDFromRaw(operation),
							Alternatives: alternatives,
						})
					}
					continue
				}
				if rootHasSecurity {
					info.SecurityRequirements = append(info.SecurityRequirements, rootSecurity)
				}
				if rootHasOAuthScopes && len(rootOAuthScopes) > 0 {
					info.OAuthScopeRequirements = append(info.OAuthScopeRequirements, oauthScopeRequirement{
						Endpoint:     strings.ToUpper(method) + " " + path,
						OperationID:  operationIDFromRaw(operation),
						Alternatives: rootOAuthScopes,
					})
				}
			}
		}
	}
	if !foundOperation && rootHasSecurity {
		info.SecurityRequirements = append(info.SecurityRequirements, rootSecurity)
	}

	for _, requirementSet := range info.SecurityRequirements {
		for _, alternative := range requirementSet.Alternatives {
			for _, name := range alternative {
				if _, ok := info.SecuritySchemes[name]; !ok {
					return info, fmt.Errorf("spec references undefined security scheme %q", name)
				}
			}
		}
	}

	return info, nil
}

func hasGraphQLEndpointExtension(raw map[string]any) bool {
	if hasNonEmptyStringExtension(raw, "x-graphql-endpoint") {
		return true
	}
	if info, ok := raw["info"].(map[string]any); ok && hasNonEmptyStringExtension(info, "x-graphql-endpoint") {
		return true
	}
	if paths, ok := raw["paths"].(map[string]any); ok {
		for _, pathValue := range paths {
			pathItem, ok := pathValue.(map[string]any)
			if !ok {
				continue
			}
			if hasNonEmptyStringExtension(pathItem, "x-graphql-endpoint") {
				return true
			}
			for method, operationValue := range pathItem {
				if !isHTTPMethod(method) {
					continue
				}
				operation, ok := operationValue.(map[string]any)
				if ok && hasNonEmptyStringExtension(operation, "x-graphql-endpoint") {
					return true
				}
			}
		}
	}
	return false
}

func hasNonEmptyStringExtension(fields map[string]any, key string) bool {
	if fields == nil {
		return false
	}
	value, ok := fields[key]
	if !ok {
		return false
	}
	return strings.TrimSpace(asString(value)) != ""
}

func operationIDFromRaw(operation map[string]any) string {
	if operation == nil {
		return ""
	}
	if id := strings.TrimSpace(asString(operation["operationId"])); id != "" {
		return id
	}
	return ""
}

type dimensionScore struct {
	score  int
	scored bool
}

func evaluatePathValidity(dir string, spec *openAPISpecInfo) dimensionScore {
	if spec == nil {
		return dimensionScore{}
	}
	if len(spec.Paths) == 0 {
		return dimensionScore{}
	}

	pathRe := regexp.MustCompile(`\bpath\s*(?::=|=|:)\s*"([^"]+)"`)
	cmdFiles := sampleCommandFiles(dir, 0) // scan all files, not a sample — avoids bias toward early-alphabet wrapper commands
	if len(cmdFiles) == 0 {
		return dimensionScore{scored: true}
	}

	total := 0
	matches := 0
	for _, content := range cmdFiles {
		found := pathRe.FindAllStringSubmatch(content, -1)
		for _, match := range found {
			if len(match) < 2 {
				continue
			}
			total++
			if specPathExists(spec.Paths, match[1]) {
				matches++
			}
		}
	}

	if total == 0 {
		return dimensionScore{scored: true}
	}
	return dimensionScore{
		score:  (matches * 10) / total,
		scored: true,
	}
}

func evaluateAuthProtocol(dir string, spec *openAPISpecInfo) dimensionScore {
	if spec == nil {
		return dimensionScore{}
	}
	clientContent := readFileContent(filepath.Join(dir, "internal", "client", "client.go"))
	configContent := readFileContent(filepath.Join(dir, "internal", "config", "config.go"))

	if len(spec.SecurityRequirements) == 0 {
		// No securitySchemes in spec. Check if auth was inferred from description
		// text (marked with "Auth inferred" comment in generated config.go).
		// Do NOT match on env var names alone — inferQueryParamAuth also produces
		// _API_KEY env vars for query-param auth, and scoring those as inferred
		// header auth would penalize correct query-param implementations.
		if !strings.Contains(configContent, "Auth inferred") {
			return dimensionScore{} // no inferred auth marker → skip scoring
		}
		// Inferred auth — score based on what the CLI actually has
		score := 1 // annotated as inferred (user knows to verify)
		// AuthProtocol pattern: inferred auth must still read an env var.
		if strings.Contains(configContent, "os.Getenv(") {
			score += 4 // env var support present
		}
		// AuthProtocol pattern: standard/custom header auth in generated client.
		if inferredAuthHeaderAssignmentPresent(clientContent) {
			score += 3 // client sends auth header (standard or custom)
		}
		// Query-param auth (e.g., TMDb ?api_key=, Google Maps ?key=):
		// the client adds the API key to the URL query string instead of a header.
		// AuthProtocol pattern: query-param auth writes known API-key parameter names.
		if strings.Contains(clientContent, `q.Set("api_key"`) ||
			strings.Contains(clientContent, `q.Set("key"`) ||
			strings.Contains(clientContent, `q.Set("apikey"`) ||
			strings.Contains(clientContent, `q.Set("apiKey"`) ||
			strings.Contains(clientContent, `params["api_key"]`) ||
			strings.Contains(clientContent, `params["apikey"]`) {
			score += 3 // client sends auth via query param
		}
		return dimensionScore{scored: true, score: score}
	}
	authContent := readFileContent(filepath.Join(dir, "internal", "cli", "auth.go"))
	if clientContent == "" {
		return dimensionScore{scored: true}
	}

	hasStructuralOAuth := hasStructuralOAuthSurface(dir, configContent)
	referencedSchemes := referencedSecuritySchemes(spec.SecurityRequirements)
	totalScore := 0
	scoredSets := 0
	for _, requirementSet := range spec.SecurityRequirements {
		if requirementSet.AllowsAnonymous {
			continue
		}

		bestScore := -1
		scoreable := false
		for _, alternative := range requirementSet.Alternatives {
			score, ok := scoreAuthAlternative(clientContent, configContent, authContent, hasStructuralOAuth, spec.SecuritySchemes, alternative, referencedSchemes)
			if !ok {
				continue
			}
			scoreable = true
			if score > bestScore {
				bestScore = score
			}
		}
		if !scoreable {
			continue
		}

		totalScore += bestScore
		scoredSets++
	}
	if scoredSets == 0 {
		return dimensionScore{}
	}
	return dimensionScore{
		score:  totalScore / scoredSets,
		scored: true,
	}
}

func parseSecurityRequirementSet(value any) (securityRequirementSet, bool) {
	requirements, ok := value.([]any)
	if !ok {
		return securityRequirementSet{}, false
	}

	set := securityRequirementSet{}
	if len(requirements) == 0 {
		set.AllowsAnonymous = true
		return set, true
	}

	seenAlternatives := make(map[string]struct{})
	for _, requirement := range requirements {
		names, ok := requirement.(map[string]any)
		if !ok {
			continue
		}
		if len(names) == 0 {
			set.AllowsAnonymous = true
			continue
		}

		var alternative []string
		for name := range names {
			if strings.TrimSpace(name) == "" {
				continue
			}
			alternative = append(alternative, name)
		}
		if len(alternative) == 0 {
			set.AllowsAnonymous = true
			continue
		}
		slices.Sort(alternative)
		key := strings.Join(alternative, "\x00")
		if _, ok := seenAlternatives[key]; ok {
			continue
		}
		seenAlternatives[key] = struct{}{}
		set.Alternatives = append(set.Alternatives, alternative)
	}

	return set, true
}

func parseOAuthScopeAlternatives(value any, schemes map[string]openAPISecurityScheme) ([]oauthScopeAlternative, bool) {
	requirements, ok := value.([]any)
	if !ok {
		return nil, false
	}

	var alternatives []oauthScopeAlternative
	seen := map[string]struct{}{}
	for _, requirement := range requirements {
		names, ok := requirement.(map[string]any)
		if !ok {
			continue
		}
		var scopes []string
		for name, scopeValue := range names {
			scheme, ok := schemes[name]
			if !ok || !isOAuthSecurityScheme(scheme) {
				continue
			}
			scopes = append(scopes, parseOAuthScopeList(scopeValue)...)
		}
		scopes = uniqueSorted(scopes)
		if len(scopes) == 0 {
			continue
		}
		key := strings.Join(scopes, "\x00")
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		alternatives = append(alternatives, oauthScopeAlternative{Scopes: scopes})
	}
	return alternatives, true
}

func parseOAuthScopeList(value any) []string {
	rawScopes, ok := value.([]any)
	if !ok {
		return nil
	}
	var scopes []string
	for _, rawScope := range rawScopes {
		scope := strings.TrimSpace(asString(rawScope))
		if scope != "" {
			scopes = append(scopes, scope)
		}
	}
	slices.Sort(scopes)
	return slices.Compact(scopes)
}

func parseScorecardAuthEnvVars(value any) []string {
	switch envVars := value.(type) {
	case []any:
		var parsed []string
		for _, raw := range envVars {
			envVar := strings.TrimSpace(asString(raw))
			if envVar != "" {
				parsed = append(parsed, envVar)
			}
		}
		return parsed
	case []string:
		var parsed []string
		for _, raw := range envVars {
			envVar := strings.TrimSpace(raw)
			if envVar != "" {
				parsed = append(parsed, envVar)
			}
		}
		return parsed
	case string:
		envVar := strings.TrimSpace(envVars)
		if envVar == "" {
			return nil
		}
		return []string{envVar}
	default:
		return nil
	}
}

func isOAuthSecurityScheme(scheme openAPISecurityScheme) bool {
	return scheme.Type == "oauth2" || scheme.Type == "openidconnect"
}

func referencedSecuritySchemes(requirementSets []securityRequirementSet) map[string]bool {
	referenced := make(map[string]bool)
	for _, requirementSet := range requirementSets {
		for _, alternative := range requirementSet.Alternatives {
			for _, key := range alternative {
				referenced[key] = true
			}
		}
	}
	return referenced
}

func scoreAuthAlternative(clientContent, configContent, authContent string, hasStructuralOAuth bool, schemes map[string]openAPISecurityScheme, alternative []string, referencedSchemes map[string]bool) (int, bool) {
	if len(alternative) == 0 {
		return 0, false
	}

	expandedAlternative := expandComposedHeaderAlternative(schemes, alternative, referencedSchemes)
	composedHeaders := isComposedHeaderAlternative(schemes, expandedAlternative)
	total := 0
	scoreableSchemes := 0
	hasZeroScore := false
	for _, key := range expandedAlternative {
		scheme, ok := schemes[key]
		if !ok {
			continue
		}
		var score int
		var scoreable bool
		if composedHeaders && isAPIKeyHeaderScheme(scheme) {
			score, scoreable = scoreComposedHeaderScheme(clientContent, scheme)
		} else {
			score, scoreable = scoreAuthScheme(clientContent, configContent, authContent, hasStructuralOAuth, scheme)
		}
		if !scoreable {
			continue
		}
		total += score
		scoreableSchemes++
		if score == 0 {
			hasZeroScore = true
		}
	}
	if scoreableSchemes == 0 {
		return 0, false
	}
	score := total / scoreableSchemes
	if hasZeroScore && scoreableSchemes > 1 && score >= 5 {
		score = 4
	}
	return score, true
}

func scoreAuthScheme(clientContent, configContent, authContent string, hasStructuralOAuth bool, scheme openAPISecurityScheme) (int, bool) {
	nameLower := strings.ToLower(scheme.Key)
	headerName := "Authorization"
	authHeaderMatched := false
	headerNameMatched := false
	queryMatched := false
	envMatched := false
	scoreable := false
	bearerStyle := false
	exactQueryParamMatched := false

	if strings.EqualFold(scheme.Type, "apikey") && (scheme.In == "header" || scheme.In == "query") && strings.TrimSpace(scheme.HeaderName) != "" {
		headerName = scheme.HeaderName
	} else if strings.EqualFold(scheme.Type, "apikey") && scheme.In == "cookie" {
		headerName = "Cookie"
	}

	switch {
	case strings.Contains(nameLower, "bot"):
		scoreable = true
		if authPrefixLiteralPresent("Bot", clientContent, configContent, authContent) {
			authHeaderMatched = true
		}
	case strings.Contains(nameLower, "bearer") || (scheme.Type == "http" && scheme.Scheme == "bearer"):
		scoreable = true
		bearerStyle = true
		bearerLiteral := scheme.Prefix
		if strings.TrimSpace(bearerLiteral) == "" {
			bearerLiteral = "Bearer"
		}
		if authPrefixLiteralPresent(bearerLiteral, clientContent, configContent, authContent) {
			authHeaderMatched = true
		}
	case strings.Contains(nameLower, "basic") || (scheme.Type == "http" && scheme.Scheme == "basic"):
		scoreable = true
		if authPrefixLiteralPresent("Basic", clientContent, configContent, authContent) {
			authHeaderMatched = true
		}
	case strings.EqualFold(scheme.Type, "apikey"):
		scoreable = true
		// For apiKey schemes, the header value format varies (Bearer, Bot, custom).
		// Credit authHeaderMatched if the client sets the correct header name,
		// since that proves the auth plumbing is wired correctly regardless of format.
		if (scheme.In == "header" || scheme.In == "cookie") && headerName != "" {
			if headerAssignmentPresent(clientContent, headerName) {
				authHeaderMatched = true
			}
		}
		if scheme.In == "query" && headerName != "" {
			if queryAssignmentPresent(clientContent, headerName) {
				exactQueryParamMatched = true
				authHeaderMatched = true
				headerNameMatched = true
				queryMatched = true
			}
		}
	case strings.EqualFold(scheme.Type, "oauth2"), strings.EqualFold(scheme.Type, "openidconnect"):
		scoreable = true
		bearerStyle = true
		if authPrefixLiteralPresent("Bearer", clientContent, configContent, authContent) {
			authHeaderMatched = true
		}
	}
	if !scoreable {
		return 0, false
	}

	// Bearer-style schemes (http/bearer, oauth2, openidconnect) are otherwise
	// scored by grepping for the "Bearer " literal and the spec's scheme key as
	// an env-var needle, both of which a cosmetic polish pass can fake by adding
	// an unused const. Real OAuth machinery (refresh-token rotation in config or
	// a dedicated oauth helper package) credits both signals at once because the
	// CLI demonstrably exchanges tokens and reads OAuth env vars (CLIENT_ID,
	// REFRESH_TOKEN) that sanitizeEnvName never matches.
	if bearerStyle && hasStructuralOAuth {
		authHeaderMatched = true
		envMatched = true
	}

	// AuthProtocol pattern: generated clients use Header.Set/Add with the expected header name.
	if headerAssignmentPresent(clientContent, headerName) {
		headerNameMatched = true
	}

	// AuthProtocol pattern: query schemes touch URL query plumbing.
	if scheme.In == "query" && (queryAssignmentPresent(clientContent, headerName) || strings.Contains(clientContent, ".Query()") || strings.Contains(clientContent, "url.Values") || strings.Contains(clientContent, "RawQuery")) {
		queryMatched = true
	}

	envNeedle := sanitizeEnvName(scheme.Key)
	// AuthProtocol pattern: config.go contains the sanitized scheme key/env name.
	if envNeedle != "" && strings.Contains(strings.ToUpper(configContent), envNeedle) {
		envMatched = true
	}
	if !envMatched && configReadsAPIKeyEnvForScheme(configContent, scheme) {
		envMatched = true
	}
	if !envMatched && configReadsSchemeEnvVar(configContent, scheme) {
		envMatched = true
	}
	// Browser cookie auth (composed or cookie type) uses Chrome cookie extraction
	// instead of env vars. Credit envMatched if the auth code has cookie tooling.
	if !envMatched && (strings.Contains(authContent, "detectCookieTool") ||
		strings.Contains(authContent, "--chrome") ||
		strings.Contains(configContent, "chrome-composed") ||
		strings.Contains(configContent, `"browser"`)) {
		envMatched = true
	}

	if strings.EqualFold(scheme.Type, "apikey") && scheme.In == "query" && strings.TrimSpace(headerName) != "" && !exactQueryParamMatched {
		return 0, true
	}

	score := 0
	if authHeaderMatched {
		score += 3
	}
	if headerNameMatched {
		score += 3
	}
	if queryMatched {
		score += 2
	}
	if envMatched {
		score += 2
	}
	if score > 10 {
		score = 10
	}
	if headerNameMatched && authHeaderMatched && envMatched {
		score = 10
	} else if headerNameMatched && authHeaderMatched && score < 8 {
		score = 8
	}
	return score, true
}

func expandComposedHeaderAlternative(schemes map[string]openAPISecurityScheme, alternative []string, referencedSchemes map[string]bool) []string {
	expanded := append([]string(nil), alternative...)
	seen := make(map[string]bool, len(alternative))
	prefixes := make(map[string]bool)
	for _, key := range alternative {
		seen[key] = true
		if prefix := composedHeaderPrefix(schemes[key]); prefix != "" {
			prefixes[prefix] = composedHeaderPrefixHasCompanion(schemes, prefix)
		}
	}
	if len(prefixes) == 0 {
		return expanded
	}
	for key, scheme := range schemes {
		if seen[key] || !isAPIKeyHeaderScheme(scheme) {
			continue
		}
		if referencedSchemes[key] {
			continue
		}
		if prefixes[composedHeaderPrefix(scheme)] {
			expanded = append(expanded, key)
		}
	}
	slices.Sort(expanded)
	return expanded
}

func composedHeaderPrefixHasCompanion(schemes map[string]openAPISecurityScheme, prefix string) bool {
	for _, scheme := range schemes {
		if composedHeaderPrefix(scheme) == prefix && composedHeaderCompanionSuffix(scheme) {
			return true
		}
	}
	return false
}

func composedHeaderCompanionSuffix(scheme openAPISecurityScheme) bool {
	if !isAPIKeyHeaderScheme(scheme) {
		return false
	}
	suffix := strings.TrimPrefix(strings.ToUpper(strings.TrimSpace(scheme.HeaderName)), composedHeaderPrefix(scheme))
	switch suffix {
	case composedHeaderSuffixSignature, composedHeaderSuffixTimestamp, composedHeaderSuffixDate, composedHeaderSuffixNonce, composedHeaderSuffixDigest:
		return true
	default:
		return false
	}
}

func isComposedHeaderAlternative(schemes map[string]openAPISecurityScheme, alternative []string) bool {
	apiKeyHeaderCount := 0
	counts := make(map[string]int)
	for _, key := range alternative {
		scheme := schemes[key]
		if isAPIKeyHeaderScheme(scheme) {
			apiKeyHeaderCount++
		}
		prefix := composedHeaderPrefix(scheme)
		if prefix != "" {
			counts[prefix]++
		}
	}
	if apiKeyHeaderCount > 1 {
		return true
	}
	for _, count := range counts {
		if count > 1 {
			return true
		}
	}
	return false
}

func composedHeaderPrefix(scheme openAPISecurityScheme) string {
	if !isAPIKeyHeaderScheme(scheme) {
		return ""
	}
	headerName := strings.ToUpper(strings.TrimSpace(scheme.HeaderName))
	idx := strings.LastIndex(headerName, "-")
	if idx < 0 {
		return ""
	}
	prefix := headerName[:idx+1]
	if strings.Count(prefix, "-") < 2 {
		return ""
	}
	return prefix
}

func isAPIKeyHeaderScheme(scheme openAPISecurityScheme) bool {
	return strings.EqualFold(scheme.Type, "apikey") && scheme.In == "header" && strings.TrimSpace(scheme.HeaderName) != ""
}

func scoreComposedHeaderScheme(clientContent string, scheme openAPISecurityScheme) (int, bool) {
	if !isAPIKeyHeaderScheme(scheme) {
		return 0, false
	}
	if headerAssignmentPresent(clientContent, scheme.HeaderName) {
		return 10, true
	}
	return 0, true
}

func headerAssignmentPresent(clientContent, headerName string) bool {
	clientContent = strings.ToLower(clientContent)
	headerName = strings.ToLower(headerName)
	return strings.Contains(clientContent, `header.set("`+headerName+`"`) ||
		strings.Contains(clientContent, `header.add("`+headerName+`"`)
}

func queryAssignmentPresent(clientContent, queryName string) bool {
	clientContent = strings.ToLower(clientContent)
	queryName = strings.ToLower(strings.TrimSpace(queryName))
	if queryName == "" {
		return false
	}
	return strings.Contains(clientContent, `q.set("`+queryName+`"`) ||
		strings.Contains(clientContent, `query.set("`+queryName+`"`) ||
		strings.Contains(clientContent, `params["`+queryName+`"]`)
}

func inferredAuthHeaderAssignmentPresent(clientContent string) bool {
	for _, headerName := range []string{"Authorization", "X-Api-Key", "X-Auth-Token", "X-Access-Token", "Cookie"} {
		if headerAssignmentPresent(clientContent, headerName) {
			return true
		}
	}
	return false
}

var (
	genericAPIKeyEnvCallRe             = regexp.MustCompile(`([A-Za-z_][A-Za-z0-9_]*)\s*:=\s*os\.Getenv\("(?:[A-Z][A-Z0-9_]*_)?API_KEY"\)`)
	directGenericAPIKeyEnvAssignmentRe = regexp.MustCompile(`\.\s*APIKey\s*=\s*os\.Getenv\("(?:[A-Z][A-Z0-9_]*_)?API_KEY"\)`)
)

func configReadsAPIKeyEnvForScheme(configContent string, scheme openAPISecurityScheme) bool {
	if !strings.EqualFold(scheme.Type, "apikey") || !isGenericAPIKeyScheme(scheme) {
		return false
	}
	if directGenericAPIKeyEnvAssignmentRe.MatchString(configContent) {
		return true
	}
	for _, match := range genericAPIKeyEnvCallRe.FindAllStringSubmatchIndex(configContent, -1) {
		if len(match) < 4 {
			continue
		}
		envVarName := configContent[match[2]:match[3]]
		tailStart := match[1]
		tailEnd := min(len(configContent), tailStart+240)
		fieldAssignmentRe := regexp.MustCompile(`\.\s*APIKey\s*=\s*` + regexp.QuoteMeta(envVarName) + `\b`)
		if fieldAssignmentRe.MatchString(configContent[tailStart:tailEnd]) {
			return true
		}
	}
	return false
}

func configReadsSchemeEnvVar(configContent string, scheme openAPISecurityScheme) bool {
	for _, envVar := range scheme.EnvVars {
		envVar = strings.TrimSpace(envVar)
		if envVar != "" && configReadsExactEnvVar(configContent, envVar) {
			return true
		}
	}
	return false
}

func configReadsExactEnvVar(configContent, envVar string) bool {
	callPattern := `\bos\.(?:Getenv|LookupEnv)\(\s*"` + regexp.QuoteMeta(envVar) + `"\s*\)`
	return regexp.MustCompile(callPattern).MatchString(configContent)
}

func isGenericAPIKeyScheme(scheme openAPISecurityScheme) bool {
	for _, name := range []string{scheme.Key, scheme.HeaderName} {
		needle := strings.ReplaceAll(sanitizeEnvName(name), "_", "")
		if strings.Contains(needle, "APIKEY") {
			return true
		}
	}
	return false
}

// refreshTokenFieldRe word-anchors RefreshToken so cosmetic names like
// NoRefreshToken or RefreshTokenError don't satisfy the structural check.
var refreshTokenFieldRe = regexp.MustCompile(`\bRefreshToken\b`)

// hasStructuralOAuthSurface returns true when the printed CLI ships real
// OAuth machinery rather than a literal "Bearer " const that grep can find.
// Either signal is sufficient — a generated config.go with a RefreshToken
// rotation field, or a hand-written internal/oauth/ helper package.
func hasStructuralOAuthSurface(dir, configContent string) bool {
	if refreshTokenFieldRe.MatchString(configContent) {
		return true
	}
	info, err := os.Stat(filepath.Join(dir, "internal", "oauth"))
	return err == nil && info.IsDir()
}

func authPrefixLiteralPresent(prefix string, contents ...string) bool {
	// AuthProtocol pattern: literal auth prefixes are emitted as quoted strings.
	doubleQuoted := `"` + prefix + ` "`
	rawQuoted := "`" + prefix + " `"
	for _, content := range contents {
		if strings.Contains(content, doubleQuoted) || strings.Contains(content, rawQuoted) {
			return true
		}
	}
	return false
}

func isHTTPMethod(method string) bool {
	switch strings.ToLower(method) {
	case "get", "put", "post", "delete", "options", "head", "patch", "trace":
		return true
	default:
		return false
	}
}

func scoreDataPipelineIntegrity(dir string) int {
	score := 0
	cliDir := filepath.Join(dir, "internal", "cli")
	allCLIContent := readAllGoFiles(cliDir)
	localDatastore := isLocalDatastoreCLIDir(dir)
	allLocalContent := allCLIContent
	if localDatastore {
		allLocalContent += readAllGoFiles(filepath.Join(dir, "internal", "source"))
		allLocalContent += readAllGoFiles(filepath.Join(dir, "internal", "store"))
		allLocalContent += readAllGoFiles(filepath.Join(dir, "internal", "mcp"))
	}
	storeContent := readFileContent(filepath.Join(dir, "internal", "store", "store.go"))
	registeredFiles := registeredCommandFiles(cliDir)
	commandContent := registeredCommandContent(cliDir, registeredFiles)
	hasGenericResourcesSearch := hasGenericResourcesSQLSearchSignalInCommands(commandContent)

	if allCLIContent != "" && (strings.Contains(allCLIContent, "/store") || strings.Contains(allCLIContent, "store.") || hasGenericResourcesSearch) {
		score++
	}

	domainUpsertRe := regexp.MustCompile(`\.Upsert[A-Z]\w*\(`)
	genericUpsertRe := regexp.MustCompile(`\.Upsert\(`)
	if domainUpsertRe.MatchString(allCLIContent) {
		score += 3
	} else if genericUpsertRe.MatchString(allCLIContent) {
		score += 0
	}

	domainSearchRe := regexp.MustCompile(`\.Search[A-Z]\w*\(`)
	genericSearchRe := regexp.MustCompile(`\.Search\(`)
	if domainSearchRe.MatchString(allCLIContent) {
		score += 3
	} else if hasGenericResourcesSearch {
		score += 3
	} else if genericSearchRe.MatchString(allCLIContent) {
		score += 0
	}

	if localDatastore && hasLocalDatastoreCodeSignalFromContent(allLocalContent) {
		if score < 8 {
			score = 8
		}
		if hasGenericResourcesSQLSearchSignal(allLocalContent) || strings.Contains(strings.ToLower(allLocalContent), "select ") {
			score += 2
		}
	}

	score += scoreDomainTables(storeContent)
	if score > 10 {
		score = 10
	}
	return score
}

func hasGenericResourcesSQLSearch(content string) bool {
	if content == "" {
		return false
	}
	if resourcesSQLSearchRE.MatchString(content) {
		return true
	}
	return resourcesFTSSQLSearchRE.MatchString(content)
}

func hasGenericResourcesSQLSearchSignal(content string) bool {
	return hasGenericResourcesSQLSearch(content) &&
		sqlQueryCallRE.MatchString(content) &&
		(rawSQLImportRe.MatchString(content) || hasStoreSignal(content))
}

func hasGenericResourcesSQLSearchSignalInCommands(commandContent map[string]string) bool {
	for _, content := range commandContent {
		if hasGenericResourcesSQLSearchSignal(content) {
			return true
		}
	}
	return false
}

func scoreSyncCorrectness(dir string) int {
	reachableFiles := scorecardReachableInternalFiles(dir)
	content := readFilesContent(reachableFiles)
	if content == "" {
		return 0
	}

	score := 0
	if hasNonEmptySyncResources(content) {
		score += 2
	}
	if strings.Contains(content, "GetSyncState") || strings.Contains(content, "sync_state") {
		score += 2
	}
	if strings.Contains(content, "SaveSyncState") {
		score++
	}
	if hasSyncPaginationStructureInFiles(reachableFiles) {
		score += 2
	}
	// URL path parameters only count when other sync signals are present,
	// otherwise any CLI with parameterized routes gets free sync credit.
	hasParamPaths := strings.Contains(content, "/{")
	if score > 0 && hasParamPaths {
		score += 3
	}
	// When the API has no parameterized list endpoints, the path-params bonus
	// is N/A. Rescale the max from 10 to 7 so flat APIs aren't penalized for
	// not having hierarchical resources.
	max := 10
	if !hasParamPaths {
		max = 7
	}
	if score > max {
		score = max
	}
	// Rescale to 0-10 range
	return score * 10 / max
}

func scoreTypeFidelity(dir string, spec *openAPISpecInfo) int {
	score := 0
	cmdFiles := sampleCommandFiles(dir, 0)
	if len(cmdFiles) == 0 {
		return 0
	}

	score += scoreTypedIDFlags(cmdFiles)
	score += scorePositionalArgHandling(cmdFiles, spec)
	if hasTypedParserCoverage(dir) {
		score += 2
	}

	if score > 5 {
		score = 5
	}
	return score
}

func scoreTypedIDFlags(cmdFiles []string) int {
	// [^,\n]+ keeps each capture inside a single Flags() call. The previous
	// [^,]+ would greedily consume across newlines into the next Flags()
	// invocation, dragging the next flag's name into the current flag's
	// description capture.
	flagDeclRe := regexp.MustCompile(`Flags\(\)\.(StringVar|IntVar|StringVarP|IntVarP)\(&[^,\n]+,\s*"([^"]+)"(?:,\s*[^,\n]+){1,2},\s*"([^"]*)"`)
	totalIDFlags := 0
	stringIDFlags := 0

	for _, content := range cmdFiles {
		for _, match := range flagDeclRe.FindAllStringSubmatch(content, -1) {
			name := strings.ToLower(match[2])
			if isIDFlagName(name) {
				totalIDFlags++
				if strings.HasPrefix(match[1], "StringVar") {
					stringIDFlags++
				}
			}
		}
	}

	switch {
	case totalIDFlags == 0:
		return 0
	case stringIDFlags == totalIDFlags:
		return 2
	case stringIDFlags > 0:
		return 1
	default:
		return 0
	}
}

func scoreCobraArgValidators(cmdFiles []string) int {
	validatorRe := regexp.MustCompile(`cobra\.(ExactArgs|MinimumNArgs|MaximumNArgs)\s*\(`)
	seen := make(map[string]struct{})
	for _, content := range cmdFiles {
		for _, match := range validatorRe.FindAllStringSubmatch(content, -1) {
			seen[match[1]] = struct{}{}
		}
	}
	if len(seen) > 2 {
		return 2
	}
	return len(seen)
}

func scorePositionalArgHandling(cmdFiles []string, spec *openAPISpecInfo) int {
	if !commandFilesAdvertisePositionals(cmdFiles) {
		return 0
	}

	score := 0
	validatorScore := scoreCobraArgValidators(cmdFiles)
	if commandFilesConsumePositionals(cmdFiles) || validatorScore > 0 {
		score++
	}
	if score > 0 && (validatorScore > 0 || (spec != nil && spec.PositionalParamCount > 0)) {
		score++
	}
	return min(score, 2)
}

func commandFilesAdvertisePositionals(cmdFiles []string) bool {
	useRe := regexp.MustCompile(`Use:\s*"[^"]*(?:<[^>]+>|\[[^\]]+\])`)
	return slices.ContainsFunc(cmdFiles, useRe.MatchString)
}

func commandFilesConsumePositionals(cmdFiles []string) bool {
	for _, content := range cmdFiles {
		if strings.Contains(content, "args[") || strings.Contains(content, "len(args)") {
			return true
		}
	}
	return false
}

func hasTypedParserCoverage(dir string) bool {
	internalDir := filepath.Join(dir, "internal")
	parserSymbolsByDir := make(map[string][]string)
	testContentByDir := make(map[string]string)

	_ = filepath.WalkDir(internalDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".go") {
			return nil
		}
		if strings.HasSuffix(d.Name(), "_test.go") {
			testContentByDir[filepath.Dir(path)] += "\n" + readFileContent(path)
			return nil
		}
		if symbols := typedParserSymbols(readFileContent(path)); len(symbols) > 0 {
			fileDir := filepath.Dir(path)
			parserSymbolsByDir[fileDir] = append(parserSymbolsByDir[fileDir], symbols...)
		}
		return nil
	})

	for fileDir, symbols := range parserSymbolsByDir {
		testContent := testContentByDir[fileDir]
		for _, symbol := range symbols {
			if strings.Contains(testContent, symbol+"(") {
				return true
			}
		}
	}
	return false
}

func typedParserSymbols(content string) []string {
	if content == "" {
		return nil
	}
	hasJSONStruct := regexp.MustCompile("(?s)type\\s+\\w+\\s+struct\\s*\\{[^}]+`json:\"").MatchString(content)
	if !hasJSONStruct {
		return nil
	}
	hasDecodeCall := strings.Contains(content, "json.Unmarshal") || strings.Contains(content, "json.NewDecoder")
	parseFuncRe := regexp.MustCompile(`func\s+(?:\([^)]+\)\s+)?(?:[Pp]arse|[Dd]ecode|[Nn]ormalize|[Uu]nmarshal)[A-Za-z0-9_]*\s*\(`)
	if !hasDecodeCall && !parseFuncRe.MatchString(content) {
		return nil
	}
	var symbols []string
	for _, match := range parseFuncRe.FindAllString(content, -1) {
		name := strings.TrimPrefix(match, "func")
		name = strings.TrimSpace(name)
		if strings.HasPrefix(name, "(") {
			if end := strings.Index(name, ")"); end >= 0 {
				name = strings.TrimSpace(name[end+1:])
			}
		}
		if open := strings.Index(name, "("); open >= 0 {
			name = strings.TrimSpace(name[:open])
		}
		if name != "" {
			symbols = append(symbols, name)
		}
	}
	return symbols
}

var pathTemplateParamRe = regexp.MustCompile(`\{[^}/]+\}`)

func countPathTemplateParams(path string) int {
	return len(pathTemplateParamRe.FindAllString(path, -1))
}

// isIDFlagName returns true when a kebab-case flag name denotes an identifier
// (id, *-id, id-*, *-id-*). Word boundaries prevent false positives like
// "price-paid-cents" matching on the "id" substring inside "paid".
func isIDFlagName(name string) bool {
	if name == "id" {
		return true
	}
	if strings.HasPrefix(name, "id-") || strings.HasSuffix(name, "-id") {
		return true
	}
	return strings.Contains(name, "-id-")
}

func scoreDeadCode(dir string) int {
	deadFlags := 0
	deadFunctions := 0
	cliDir := filepath.Join(dir, "internal", "cli")
	rootContent := readFileContent(filepath.Join(cliDir, "root.go"))
	helpersContent := readFileContent(filepath.Join(cliDir, "helpers.go"))
	if rootContent == "" && helpersContent == "" {
		return 0
	}

	flagRe := regexp.MustCompile(`&flags\.(\w+)`)
	flagNames := uniqueMatches(flagRe, rootContent)
	otherCLI := readOtherGoFiles(cliDir, map[string]bool{"root.go": true})

	// If the flags struct is passed as a function argument, all fields are reachable
	flagsPassedRe := regexp.MustCompile(`\bflags[,)]`)
	flagsPassedAsArg := flagsPassedRe.MatchString(otherCLI)
	if !flagsPassedAsArg {
		for _, name := range flagNames {
			if !strings.Contains(otherCLI, "flags."+name) {
				deadFlags++
			}
		}
	}

	funcRe := regexp.MustCompile(`(?m)^func\s+([A-Za-z_]\w*)\s*\(`)
	funcNames := uniqueMatches(funcRe, helpersContent)
	otherHelpers := readOtherGoFiles(cliDir, map[string]bool{"helpers.go": true})
	// Check both other files AND helpers.go itself for intra-file calls.
	// Use Count >= 2 because the definition itself contributes 1 occurrence of name+"(".
	allContent := helpersContent + "\n" + otherHelpers
	for _, name := range funcNames {
		if isAllowedDeadHelper(name) {
			continue
		}
		if strings.Count(allContent, name+"(") < 2 {
			deadFunctions++
		}
	}

	score := 5 - (deadFlags + deadFunctions)
	if score < 0 {
		return 0
	}
	return score
}

// sampleCommandFiles reads up to n command files from internal/cli/.
// If n <= 0, reads all command files.
func sampleCommandFiles(dir string, n int) []string {
	cliDir := filepath.Join(dir, "internal", "cli")
	entries, err := os.ReadDir(cliDir)
	if err != nil {
		return nil
	}
	var files []string
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") {
			continue
		}
		if infraAllFiles[e.Name()] {
			continue
		}
		content := readFileContent(filepath.Join(cliDir, e.Name()))
		if content != "" {
			files = append(files, content)
		}
		if n > 0 && len(files) >= n {
			break
		}
	}
	return files
}

func specPathExists(specPaths []string, actual string) bool {
	for _, candidate := range specPaths {
		if matchSpecPath(candidate, actual) || matchSpecPath(actual, candidate) {
			return true
		}
	}
	return false
}

func matchSpecPath(pattern, actual string) bool {
	patternParts := splitPath(pattern)
	actualParts := splitPath(actual)
	if len(patternParts) != len(actualParts) {
		return false
	}
	for i := range patternParts {
		part := patternParts[i]
		if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
			continue
		}
		if part != actualParts[i] {
			return false
		}
	}
	return true
}

func splitPath(path string) []string {
	trimmed := strings.Trim(path, "/")
	if trimmed == "" {
		return nil
	}
	return strings.Split(trimmed, "/")
}

func sanitizeEnvName(name string) string {
	name = strings.ToUpper(name)
	var b strings.Builder
	lastUnderscore := false
	for _, r := range name {
		if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
			b.WriteRune(r)
			lastUnderscore = false
			continue
		}
		if !lastUnderscore {
			b.WriteByte('_')
			lastUnderscore = true
		}
	}
	return strings.Trim(b.String(), "_")
}

func scoreDomainTables(storeContent string) int {
	if storeContent == "" {
		return 0
	}
	createTableRe := regexp.MustCompile(`(?is)CREATE TABLE[^()]*\((.*?)\)`)
	columnTables := 0
	for _, match := range createTableRe.FindAllStringSubmatch(storeContent, -1) {
		columnCount := 0
		for line := range strings.SplitSeq(match[1], "\n") {
			line = strings.TrimSpace(line)
			if line == "" || strings.HasPrefix(line, "--") {
				continue
			}
			upper := strings.ToUpper(line)
			if strings.HasPrefix(upper, "PRIMARY KEY") || strings.HasPrefix(upper, "FOREIGN KEY") || strings.HasPrefix(upper, "UNIQUE") || strings.HasPrefix(upper, "CONSTRAINT") {
				continue
			}
			columnCount++
		}
		if columnCount >= 5 {
			columnTables++
		}
	}
	if columnTables > 0 {
		return 3
	}
	return 0
}

func hasNonEmptySyncResources(content string) bool {
	if !strings.Contains(content, "defaultSyncResources") && !strings.Contains(content, "syncResources") {
		return false
	}
	// Look for non-empty []string{...} literals (sync resource lists)
	listRe := regexp.MustCompile(`\[\]string\{([^}]*)\}`)
	for _, match := range listRe.FindAllStringSubmatch(content, -1) {
		items := strings.TrimSpace(match[1])
		if items != "" {
			return true
		}
	}
	// If defaultSyncResources is called but its definition isn't in the content,
	// assume it's non-empty (defined in a different package/file).
	// If the definition IS here, the listRe above already checked all []string{} literals.
	defRe := regexp.MustCompile(`func\s+defaultSyncResources\s*\(`)
	if strings.Contains(content, "defaultSyncResources()") && !defRe.MatchString(content) {
		return true
	}
	return false
}

func uniqueMatches(re *regexp.Regexp, content string) []string {
	seen := map[string]bool{}
	var out []string
	for _, match := range re.FindAllStringSubmatch(content, -1) {
		if len(match) < 2 || seen[match[1]] {
			continue
		}
		seen[match[1]] = true
		out = append(out, match[1])
	}
	return out
}

// readAllGoFiles concatenates the content of all .go files in dir.
func readAllGoFiles(dir string) string {
	return readFilesContent(listGoFiles(dir))
}

func readFilesContent(paths []string) string {
	var b strings.Builder
	for _, path := range paths {
		b.WriteString(readFileContent(path))
		b.WriteByte('\n')
	}
	return b.String()
}

func readOtherGoFiles(dir string, skip map[string]bool) string {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return ""
	}
	var b strings.Builder
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || skip[entry.Name()] {
			continue
		}
		b.WriteString(readFileContent(filepath.Join(dir, entry.Name())))
		b.WriteByte('\n')
	}
	return b.String()
}

func asString(v any) string {
	switch value := v.(type) {
	case string:
		return value
	case fmt.Stringer:
		return value.String()
	case float64:
		return strconv.FormatFloat(value, 'f', -1, 64)
	default:
		return ""
	}
}

// hasPlaceholderValues checks if file content contains common placeholder values
// that indicate unpolished examples.
func hasPlaceholderValues(content string) bool {
	placeholders := []string{"abc123", `"value"`, "my-resource", "your-key-here", "USER/tap"}
	for _, p := range placeholders {
		if strings.Contains(content, p) {
			return true
		}
	}
	return false
}

// hasQualityDescription checks if a command file has a meaningful Short description.
// Returns true if the description is multi-word and doesn't just repeat the verb.
func hasQualityDescription(content string) bool {
	idx := strings.Index(content, "Short:")
	if idx < 0 {
		return false
	}
	// Extract the Short value (between quotes)
	rest := content[idx:]
	q1 := strings.Index(rest, `"`)
	if q1 < 0 {
		return false
	}
	q2 := strings.Index(rest[q1+1:], `"`)
	if q2 < 0 {
		return false
	}
	desc := rest[q1+1 : q1+1+q2]
	// Minimum quality: multi-word and non-trivial length.
	// Actual description quality (informative vs boilerplate) is handled by
	// the skill instruction during Phase 3 polish, not by this scorer.
	return len(desc) > 10 && strings.Contains(desc, " ")
}

// hasLazyDescription checks if a command has a 1-word or very short description.
func hasLazyDescription(content string) bool {
	idx := strings.Index(content, "Short:")
	if idx < 0 {
		return false
	}
	rest := content[idx:]
	q1 := strings.Index(rest, `"`)
	if q1 < 0 {
		return false
	}
	q2 := strings.Index(rest[q1+1:], `"`)
	if q2 < 0 {
		return false
	}
	desc := rest[q1+1 : q1+1+q2]
	words := strings.Fields(desc)
	return len(words) <= 2
}

func readFileContent(path string) string {
	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	return string(data)
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func isLocalDatastoreCLIDir(dir string) bool {
	manifest, err := loadCLIManifestForScorecard(dir)
	return err == nil && manifest.IsLocalDatastore()
}

func isDeviceBackedCLIDir(dir string) bool {
	if looksLikeDeviceSpecFile(filepath.Join(dir, "device-spec.yaml")) {
		return true
	}
	if strings.Contains(readFileContent(filepath.Join(dir, "README.md")), "Generated by the Printing Press from a BLE device spec.") {
		return true
	}
	return strings.Contains(readFileContent(filepath.Join(dir, "internal", "device", "spec.go")), `Protocol = "ble"`)
}

func looksLikeDeviceSpecFile(path string) bool {
	if strings.TrimSpace(path) == "" {
		return false
	}
	data, err := os.ReadFile(path)
	return err == nil && devicespec.LooksLikeDeviceSpec(data)
}

func hasLocalDatastoreCodeSignal(dir string) bool {
	var b strings.Builder
	for _, rel := range []string{
		filepath.Join("internal", "cli"),
		filepath.Join("internal", "source"),
		filepath.Join("internal", "store"),
		filepath.Join("internal", "mcp"),
	} {
		b.WriteString(readAllGoFiles(filepath.Join(dir, rel)))
		b.WriteByte('\n')
	}
	return hasLocalDatastoreCodeSignalFromContent(b.String())
}

func hasLocalDatastoreCodeSignalFromContent(content string) bool {
	lower := strings.ToLower(content)
	return strings.Contains(lower, "sqlite") ||
		strings.Contains(lower, "database/sql") ||
		strings.Contains(lower, "sql.open")
}

func computeGrade(percentage int) string {
	switch {
	case percentage >= 80:
		return "A"
	case percentage >= 65:
		return "B"
	case percentage >= 50:
		return "C"
	case percentage >= 35:
		return "D"
	default:
		return "F"
	}
}

// loadCLIManifestForScorecard reads .printing-press.json from the CLI directory.
// Returns an empty manifest (not error) if the file does not exist, so callers
// can check MCPBinary != "" to decide whether to show MCP info.
func loadCLIManifestForScorecard(outputDir string) (CLIManifest, error) {
	data, err := os.ReadFile(filepath.Join(outputDir, CLIManifestFilename))
	if err != nil {
		return CLIManifest{}, err
	}
	var m CLIManifest
	if err := json.Unmarshal(data, &m); err != nil {
		return CLIManifest{}, err
	}
	return m, nil
}

func buildGapReport(s SteinerScore, unscored []string) []string {
	var gaps []string
	unscoredSet := make(map[string]struct{}, len(unscored))
	for _, name := range unscored {
		unscoredSet[name] = struct{}{}
	}
	dimensions := []struct {
		name  string
		score int
	}{
		{"output_modes", s.OutputModes},
		{"auth", s.Auth},
		{"error_handling", s.ErrorHandling},
		{"terminal_ux", s.TerminalUX},
		{"readme", s.README},
		{"doctor", s.Doctor},
		{"agent_native", s.AgentNative},
		{"mcp_quality", s.MCPQuality},
		{DimMCPDescriptionQuality, s.MCPDescriptionQuality},
		{DimMCPTokenEfficiency, s.MCPTokenEff},
		{"local_cache", s.LocalCache},
		{"breadth", s.Breadth},
		{"vision", s.Vision},
		{"workflows", s.Workflows},
		{"insight", s.Insight},
		{DimPathValidity, s.PathValidity},
		{DimAuthProtocol, s.AuthProtocol},
		{"data_pipeline_integrity", s.DataPipelineIntegrity},
		{"sync_correctness", s.SyncCorrectness},
		{"type_fidelity", s.TypeFidelity},
		{DimDeadCode, s.DeadCode},
	}
	for _, d := range dimensions {
		if _, skip := unscoredSet[d.name]; skip {
			continue
		}
		max := 10
		if d.name == DimTypeFidelity || d.name == DimDeadCode {
			max = 5
		}
		if d.score < max/2 {
			gaps = append(gaps, fmt.Sprintf("%s scored %d/%d - needs improvement", d.name, d.score, max))
		}
	}
	return gaps
}

func buildCompetitorScores(ourTotal int, artifactDir string) []CompScore {
	research, err := loadResearchForArtifactsDir(artifactDir)
	if err != nil {
		return nil
	}
	var scores []CompScore
	for _, alt := range research.Alternatives {
		theirScore := estimateCompetitorTotal(alt)
		scores = append(scores, CompScore{
			Name:       alt.Name,
			OurScore:   ourTotal,
			TheirScore: theirScore,
			WeWin:      ourTotal > theirScore,
		})
	}
	return scores
}

func loadResearchForArtifactsDir(artifactDir string) (*ResearchResult, error) {
	parent := filepath.Dir(artifactDir)
	var candidates []string
	switch filepath.Base(artifactDir) {
	case "research":
		candidates = []string{artifactDir, filepath.Join(parent, "pipeline")}
	case "proofs", "pipeline":
		candidates = []string{filepath.Join(parent, "research"), artifactDir, filepath.Join(parent, "pipeline")}
	default:
		candidates = []string{artifactDir, filepath.Join(artifactDir, "research"), filepath.Join(artifactDir, "pipeline")}
	}

	var lastErr error
	seen := make(map[string]struct{}, len(candidates))
	for _, candidate := range candidates {
		if _, ok := seen[candidate]; ok {
			continue
		}
		seen[candidate] = struct{}{}

		research, err := LoadResearch(candidate)
		if err == nil {
			return research, nil
		}
		lastErr = err
	}

	if lastErr == nil {
		lastErr = os.ErrNotExist
	}
	return nil, lastErr
}

func estimateCompetitorTotal(alt Alternative) int {
	score := 0
	if alt.HasJSON {
		score += 6 // output_modes partial credit
	}
	if alt.HasAuth {
		score += 5 // auth partial credit
	}
	// Assume basic error handling and terminal UX
	score += 3
	score += 3
	// README and doctor are unknowns - give partial credit
	score += 4
	score += 2
	// Agent native: partial if they have JSON
	if alt.HasJSON {
		score += 3
	}
	return score
}

func writeScorecardMD(sc *Scorecard, pipelineDir string) error {
	if err := os.MkdirAll(pipelineDir, 0o755); err != nil {
		return err
	}

	var b strings.Builder
	fmt.Fprintf(&b, "# Scorecard: %s\n\n", sc.APIName)
	fmt.Fprintf(&b, "**Overall Grade: %s** (%d%%)\n\n", sc.OverallGrade, sc.Steinberger.Percentage)
	if len(sc.UnscoredDimensions) > 0 {
		fmt.Fprintf(&b, "Unscored dimensions omitted from the total denominator: %s\n\n", strings.Join(sc.UnscoredDimensions, ", "))
	}

	// Steinberger dimensions table
	b.WriteString("## Quality Dimensions\n\n")
	b.WriteString("| Dimension | Score |\n")
	b.WriteString("|-----------|-------|\n")
	s := sc.Steinberger
	dimensions := []struct {
		name    string
		nameKey string
		score   int
	}{
		{"Output Modes", "output_modes", s.OutputModes},
		{"Auth", "auth", s.Auth},
		{"Error Handling", "error_handling", s.ErrorHandling},
		{"Terminal UX", "terminal_ux", s.TerminalUX},
		{"README", "readme", s.README},
		{"Doctor", "doctor", s.Doctor},
		{"Agent Native", "agent_native", s.AgentNative},
		{"MCP Quality", "mcp_quality", s.MCPQuality},
		{"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", DimPathValidity, s.PathValidity},
		{"Auth Protocol", DimAuthProtocol, s.AuthProtocol},
		{"Data Pipeline Integrity", "data_pipeline_integrity", s.DataPipelineIntegrity},
		{"Sync Correctness", "sync_correctness", s.SyncCorrectness},
	}
	for _, d := range dimensions {
		if sc.IsDimensionUnscored(d.nameKey) {
			fmt.Fprintf(&b, "| %s | N/A |\n", d.name)
			continue
		}
		bar := strings.Repeat("#", d.score) + strings.Repeat(".", 10-d.score)
		fmt.Fprintf(&b, "| %s | %d/10 %s |\n", d.name, d.score, bar)
	}
	typeDimensions := []struct {
		name  string
		score int
	}{
		{"Type Fidelity", s.TypeFidelity},
		{"Dead Code", s.DeadCode},
	}
	for _, d := range typeDimensions {
		if sc.IsDimensionUnscored(strings.ToLower(strings.ReplaceAll(d.name, " ", "_"))) {
			fmt.Fprintf(&b, "| %s | N/A |\n", d.name)
			continue
		}
		bar := strings.Repeat("#", d.score) + strings.Repeat(".", 5-d.score)
		fmt.Fprintf(&b, "| %s | %d/5 %s |\n", d.name, d.score, bar)
	}
	fmt.Fprintf(&b, "| **Total** | **%d/100** |\n\n", s.Total)

	// Competitor comparison
	if len(sc.CompetitorScores) > 0 {
		b.WriteString("## Competitor Comparison\n\n")
		b.WriteString("| Competitor | Ours | Theirs | Winner |\n")
		b.WriteString("|------------|------|--------|--------|\n")
		for _, cs := range sc.CompetitorScores {
			winner := "Them"
			if cs.WeWin {
				winner = "Us"
			}
			fmt.Fprintf(&b, "| %s | %d | %d | %s |\n", cs.Name, cs.OurScore, cs.TheirScore, winner)
		}
		b.WriteString("\n")
	}

	// Gap report
	if len(sc.GapReport) > 0 {
		b.WriteString("## Gaps\n\n")
		for _, g := range sc.GapReport {
			fmt.Fprintf(&b, "- %s\n", g)
		}
		b.WriteString("\n")
	}

	return os.WriteFile(filepath.Join(pipelineDir, "scorecard.md"), []byte(b.String()), 0o644)
}

func writeScorecardJSON(sc *Scorecard, pipelineDir string) error {
	if err := os.MkdirAll(pipelineDir, 0o755); err != nil {
		return err
	}

	data, err := json.MarshalIndent(sc, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(filepath.Join(pipelineDir, "scorecard.json"), data, 0o644)
}

// LoadScorecard reads a scorecard from a pipeline directory's scorecard.json.
func LoadScorecard(pipelineDir string) (*Scorecard, error) {
	data, err := os.ReadFile(filepath.Join(pipelineDir, "scorecard.json"))
	if err != nil {
		return nil, err
	}
	var sc Scorecard
	if err := json.Unmarshal(data, &sc); err != nil {
		return nil, err
	}
	return &sc, nil
}
