Skip to content

🔤 Typist - Go Type Consistency Analysis #15134

@github-actions

Description

@github-actions

Executive Summary

Analysis of 491 Go files in the pkg/ directory revealed extensive use of untyped map[string]any structures for configuration parsing, with 200+ function signatures accepting weakly-typed parameters. While the codebase exhibits excellent type organization with 417+ well-named type definitions and minimal duplication, there are significant opportunities to improve type safety in configuration parsing and data processing layers.

Key Findings:

  • Zero interface{} usage - The codebase consistently uses any (Go 1.18+)
  • ⚠️ 200+ functions accept map[string]any or any parameters requiring runtime type assertions
  • ⚠️ 1 duplicated type (MCPServerConfig) defined in both workflow and parser packages
  • Strong type structure - Configuration types are well-organized with clear naming conventions
  • ⚠️ Heavy reliance on type assertions in YAML/JSON parsing logic

Impact Priority:

  1. High: Function parameters using map[string]any (type safety risk)
  2. Medium: Duplicated MCPServerConfig type (maintenance confusion)
  3. Low: Generic helper functions appropriately using any
Full Analysis Report

Duplicated Type Definitions

Summary Statistics

  • Total types analyzed: 417+
  • Duplicate clusters found: 1
  • Exact duplicates: 0
  • Semantic duplicates: 1 (MCPServerConfig)

Cluster 1: MCPServerConfig Duplication

Type: Semantic duplicate with different fields
Occurrences: 2
Impact: Medium - Same base type, different package-specific extensions

Locations:

  1. pkg/workflow/tools_types.go:350 - Workflow-specific MCPServerConfig
  2. pkg/parser/mcp.go:83 - Parser-specific MCPServerConfig

Definition Comparison:

// Base type (shared)
// pkg/types/mcp.go:5
type BaseMCPServerConfig struct {
    Command string
    Args    []string
    Env     map[string]string
    Type    string
    Version string
    URL     string
    Headers map[string]string
    Container      string
    Entrypoint     string
    EntrypointArgs []string
    Mounts         []string
}

// Workflow package extension
// pkg/workflow/tools_types.go:350
type MCPServerConfig struct {
    types.BaseMCPServerConfig
    Mode     string   // MCP server mode (stdio, http, remote, local)
    Toolsets []string // Toolsets to enable
    CustomFields map[string]any `yaml:",inline"`
}

// Parser package extension
// pkg/parser/mcp.go:83
type MCPServerConfig struct {
    types.BaseMCPServerConfig
    Name      string   // Server name/identifier
    Registry  string   // URI to installation location from registry
    ProxyArgs []string // custom proxy arguments for container-based tools
    Allowed   []string // allowed tools
}

Analysis:
Both packages define their own MCPServerConfig by embedding the shared BaseMCPServerConfig. This is not necessarily problematic - it follows the pattern of domain-specific type extensions. However, it creates potential confusion:

  • Different fields in each package (Mode/Toolsets vs Name/Registry/ProxyArgs/Allowed)
  • Cannot pass one type to functions expecting the other
  • Potential for accidental misuse if imports are mixed

Recommendation:

  • Option 1 (Recommended): Rename types to reflect domain - WorkflowMCPServerConfig and ParserMCPServerConfig
  • Option 2: If the types serve similar purposes, unify into a single type in pkg/types/ with all fields
  • Estimated effort: 1-2 hours (rename + update references)
  • Benefits: Clearer intent, prevents import confusion

Untyped Usages

Summary Statistics

  • interface{} usages: 0 ✅
  • Functions with any parameters: 200+
  • map[string]any usages: 150+ occurrences
  • []any usages: 20+ occurrences
  • Primary use case: YAML/JSON unmarshaling and configuration parsing

Category 1: map[string]any for Configuration Parsing

Impact: High - Runtime type assertions throughout configuration layer
Frequency: ~150+ function signatures

This is the dominant pattern in the codebase. Configuration data from YAML frontmatter is unmarshaled into map[string]any and then parsed with type assertions.

Examples:

Example 1: Frontmatter Processing Functions

Location: pkg/workflow/frontmatter_types.go (50+ usages)

// Current pattern
func ParseFrontmatterConfig(frontmatter map[string]any) (*FrontmatterConfig, error)
func parseRuntimesConfig(runtimes map[string]any) (*RuntimesConfig, error)
func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, error)
func ExtractMapField(frontmatter map[string]any, key string) map[string]any
func ExtractStringField(frontmatter map[string]any, key string) string
func ExtractIntField(frontmatter map[string]any, key string) int

Why this exists: YAML unmarshaling produces map[string]any by default when structure isn't known upfront.

Suggested approach:

// Define explicit YAML structure types
type FrontmatterYAML struct {
    Runtimes    map[string]RuntimeConfig    `yaml:"runtimes,omitempty"`
    Permissions map[string]PermissionsField `yaml:"permissions,omitempty"`
    Tools       map[string]ToolConfig       `yaml:"tools,omitempty"`
    // ... other fields
}

// Then parse directly
func ParseFrontmatterConfig(yamlData []byte) (*FrontmatterConfig, error) {
    var fm FrontmatterYAML
    if err := yaml.Unmarshal(yamlData, &fm); err != nil {
        return nil, err
    }
    // Convert to internal config structure
    return convertToConfig(&fm), nil
}

Trade-offs:

  • Pro: Compile-time type safety, no type assertions
  • Pro: Better IDE support and autocomplete
  • Con: Requires defining complete YAML schema upfront
  • Con: Less flexible for dynamic/unknown fields
  • ⚠️ Note: Current pattern allows flexible field handling
Example 2: Tool Configuration Parsing

Location: pkg/workflow/tools_parser.go:61

// Current (untyped)
func NewTools(toolsMap map[string]any) *Tools {
    // Extract with type assertions
    if val, exists := toolsMap["github"]; exists {
        tools.GitHub = parseGitHubTool(val) // val is any
    }
    if val, exists := toolsMap["bash"]; exists {
        tools.Bash = parseBashTool(val) // val is any
    }
    // ... repeated for each tool type
}

func parseGitHubTool(val any) *GitHubToolConfig {
    switch v := val.(type) {
    case string:
        return &GitHubToolConfig{Type: v}
    case map[string]any:
        // Parse map structure
    case bool:
        // Handle boolean shorthand
    }
    return nil
}

Suggested strongly-typed alternative:

// Define YAML union type
type ToolConfigValue struct {
    StringValue *string
    MapValue    map[string]any
    BoolValue   *bool
}

// Or use direct struct unmarshaling
type ToolsYAML struct {
    GitHub    json.RawMessage `yaml:"github,omitempty"`
    Bash      json.RawMessage `yaml:"bash,omitempty"`
    Playwright json.RawMessage `yaml:"playwright,omitempty"`
    // ... more tools
}

func NewTools(toolsData []byte) (*Tools, error) {
    var ty ToolsYAML
    if err := yaml.Unmarshal(toolsData, &ty); err != nil {
        return nil, err
    }
    
    tools := &Tools{}
    if len(ty.GitHub) > 0 {
        tools.GitHub = parseGitHubToolFromJSON(ty.GitHub)
    }
    // ...
    return tools, nil
}

Benefits: Type-safe parsing, clear structure
Effort: High (4-6 hours) - Requires restructuring parsing logic

Example 3: Config Helper Functions

Location: pkg/workflow/config_helpers.go

// Current (15+ similar functions)
func ParseStringArrayFromConfig(m map[string]any, key string, log *logger.Logger) []string
func ParseIntFromConfig(m map[string]any, key string, log *logger.Logger) int
func ParseBoolFromConfig(m map[string]any, key string, log *logger.Logger) bool
func parseLabelsFromConfig(configMap map[string]any) []string
func parseTitlePrefixFromConfig(configMap map[string]any) string
func parseTargetRepoFromConfig(configMap map[string]any) string

These are generic field extractors that perform type assertions internally. They exist because the config is map[string]any.

Alternative: If configs were strongly typed, these helpers wouldn't be needed:

// With strong typing
type Config struct {
    Labels      []string `yaml:"labels"`
    TitlePrefix string   `yaml:"title_prefix"`
    TargetRepo  string   `yaml:"target_repo"`
}

// No helper needed - direct field access
func processConfig(cfg *Config) {
    labels := cfg.Labels  // Type-safe
    prefix := cfg.TitlePrefix  // Type-safe
}

Category 2: Functions Accepting any Parameters

Impact: Medium - Type assertions required at call sites
Frequency: 50+ function signatures

Examples:

Example 1: Tool Value Parsers

Locations: pkg/workflow/tools_parser.go, pkg/workflow/mcp_github_config.go

// Current
func parseGitHubTool(val any) *GitHubToolConfig
func parseBashTool(val any) *BashToolConfig
func parsePlaywrightTool(val any) *PlaywrightToolConfig
func parseSerenaTool(val any) *SerenaToolConfig

func getGitHubType(githubTool any) string
func getGitHubToken(githubTool any) string
func getGitHubReadOnly(githubTool any) bool
func getGitHubToolsets(githubTool any) string

Why any? These functions handle multiple input formats (string, bool, map) for flexibility:

// Valid YAML inputs:
tools:
  github: true                    # bool - enable with defaults
  github: "readonly"              # string - shorthand mode
  github:                         # map - full config
    type: lockdown
    token: $\{\{ secrets.TOKEN }}

This is appropriate use of any for polymorphic configuration. The alternative would be:

// Type-safe but verbose
type GitHubToolInput interface {
    isGitHubToolInput()
}
type GitHubToolBool bool
func (GitHubToolBool) isGitHubToolInput() {}
type GitHubToolString string
func (GitHubToolString) isGitHubToolInput() {}
type GitHubToolMap map[string]any
func (GitHubToolMap) isGitHubToolInput() {}

func parseGitHubTool(val GitHubToolInput) *GitHubToolConfig

Recommendation: Keep current approach - The any parameter is justified for handling multiple input types. The type assertions are well-contained within parser functions.

Example 2: Generic Conversion Utilities

Location: pkg/workflow/metrics.go:211

// Current
func ConvertToInt(val any) int {
    switch v := val.(type) {
    case int:
        return v
    case int64:
        return int(v)
    case float64:
        return int(v)
    case string:
        if i, err := strconv.Atoi(v); err == nil {
            return i
        }
    }
    return 0
}

func ConvertToFloat(val any) float64 {
    switch v := val.(type) {
    case float64:
        return v
    case int:
        return float64(v)
    case int64:
        return float64(v)
    case string:
        if f, err := strconv.ParseFloat(v, 64); err == nil {
            return f
        }
    }
    return 0.0
}

Analysis: These are appropriate uses of any - they're generic conversion utilities that handle multiple numeric types. Similar to fmt.Println accepting any.

Recommendation: Keep as-is - These are legitimate generic functions.


Category 3: []any for Dynamic Arrays

Impact: Medium
Frequency: 20+ occurrences

Example: pkg/workflow/inputs.go:60

// Current
func ParseInputDefinition(inputConfig map[string]any) *InputDefinition {
    if opts, exists := inputConfig["options"]; exists {
        if optsList, ok := opts.([]any); ok {
            for _, opt := range optsList {
                if optStr, ok := opt.(string); ok {
                    input.Options = append(input.Options, optStr)
                }
            }
        }
    }
}

Suggested:

// If inputConfig were strongly typed:
type InputDefinitionYAML struct {
    Description string   `yaml:"description"`
    Required    bool     `yaml:"required"`
    Default     any      `yaml:"default"`  // Legitimately can be any type
    Type        string   `yaml:"type"`
    Options     []string `yaml:"options"`
}

func ParseInputDefinition(input *InputDefinitionYAML) *InputDefinition {
    return &InputDefinition{
        Description: input.Description,
        Required:    input.Required,
        Default:     input.Default,
        Type:        input.Type,
        Options:     input.Options,  // No type assertion needed
    }
}

Analysis by Package

pkg/workflow (Primary Source of map[string]any)

Files with highest usage:

  • frontmatter_types.go: 50+ occurrences
  • config_helpers.go: 30+ occurrences
  • tools_parser.go: 25+ occurrences
  • mcp_config_custom.go: 20+ occurrences

Pattern: Centralized YAML frontmatter parsing layer that converts untyped maps to strongly-typed configuration structs.

pkg/parser

Files with high usage:

  • schema_validation.go: 15+ occurrences
  • frontmatter_hash.go: 15+ occurrences
  • import_processor.go: 12+ occurrences

Pattern: Schema validation and import processing that works with generic YAML structures.

pkg/cli

Files with usage:

  • mcp_add.go, imports.go, devcontainer.go

Pattern: CLI command helpers that manipulate configuration files.


Refactoring Recommendations

Priority 1: Document Intended Pattern (Low Effort, High Value)

Recommendation: Add package-level documentation explaining the parsing strategy

Location: pkg/workflow/doc.go or similar

/*
Package workflow implements agentic workflow compilation.

## Configuration Parsing Strategy

This package uses a two-stage parsing approach:
1. YAML unmarshaling produces map[string]any for flexibility
2. Type-safe parsing functions convert to strongly-typed structs

This pattern allows:
- Flexible YAML input formats (bool, string, map shortcuts)
- Runtime validation with helpful error messages
- Strong typing for internal logic

Functions accepting map[string]any or any parameters are intentionally
designed for configuration parsing and should perform type assertions
safely with appropriate error handling.
*/
package workflow

Estimated effort: 30 minutes
Impact: Medium - Clarifies design intent for maintainers


Priority 2: Rename Duplicated MCPServerConfig (Medium Effort)

Recommendation: Rename to domain-specific names

Steps:

  1. pkg/workflow/tools_types.go: Rename to WorkflowMCPServerConfig
  2. pkg/parser/mcp.go: Rename to ParserMCPServerConfig
  3. Update all references in both packages
  4. Update tests

Estimated effort: 1-2 hours
Impact: Medium - Prevents confusion, clearer intent


Priority 3: Consider Typed YAML Schemas (High Effort, Optional)

Recommendation: For new features, consider defining explicit YAML schema types

Example pattern:

// Define YAML schema
type NewFeatureYAML struct {
    Field1 string   `yaml:"field1"`
    Field2 int      `yaml:"field2"`
    Field3 []string `yaml:"field3"`
}

// Parse directly
func ParseNewFeature(yamlData []byte) (*NewFeatureConfig, error) {
    var yaml NewFeatureYAML
    if err := yaml.Unmarshal(yamlData, &yaml); err != nil {
        return nil, err
    }
    return convertToConfig(&yaml), nil
}

When to use:

  • ✅ New features with well-defined schema
  • ✅ Configuration that doesn't need flexibility
  • ❌ Existing frontmatter parsing (too much refactoring)
  • ❌ Polymorphic tool configs (multiple input formats)

Estimated effort: Per-feature basis
Impact: High (for new code) - Compile-time type safety


Summary Assessment

What's Working Well ✅

  1. Strong type organization - 417+ well-named configuration types
  2. Consistent naming - *Config suffix convention
  3. Zero interface{} usage - Modern Go with any
  4. Shared base types - BaseMCPServerConfig pattern for composition
  5. Contained type assertions - All map[string]any handling is in dedicated parsing layers

Opportunities for Improvement ⚠️

  1. Document parsing strategy - Make intentional pattern explicit
  2. Rename MCPServerConfig - Eliminate name collision between packages
  3. Consider typed schemas for new features - Gradual migration to stronger typing

What NOT to Change ❌

  1. Don't eliminate map[string]any in existing frontmatter parsing - It's a deliberate design for flexible YAML handling
  2. Don't type any parameters in polymorphic parsers - They handle multiple input formats by design
  3. Don't add types to generic utilities like ConvertToInt - They're appropriately generic

Implementation Checklist

  • Add package documentation explaining map[string]any parsing strategy
  • Rename workflow.MCPServerConfig to WorkflowMCPServerConfig
  • Rename parser.MCPServerConfig to ParserMCPServerConfig
  • Update all references and imports
  • Run full test suite
  • For new features: Consider using explicit YAML schema types
  • Update contributing guide with type safety guidelines

Analysis Metadata

  • Total Go Files Analyzed: 491
  • Total Type Definitions: 417+
  • Duplicate Type Names: 1 (MCPServerConfig)
  • Functions with any parameters: 200+
  • map[string]any occurrences: 150+
  • interface{} occurrences: 0 ✅
  • Detection Method: Serena semantic analysis + pattern search
  • Analysis Date: 2026-02-12
  • Repository: github/gh-aw
  • Primary Finding: Intentional use of map[string]any for flexible YAML parsing - not a type safety issue, but an architectural choice

Conclusion

The codebase demonstrates excellent type organization and minimal duplication. The extensive use of map[string]any is intentional and appropriate for flexible YAML configuration parsing. The primary recommendation is to document this pattern and rename the duplicated MCPServerConfig type for clarity. The type consistency is actually quite strong - the map[string]any usage is well-contained in parsing layers and doesn't leak into business logic.

Overall Grade: 🟢 Strong - Well-structured types with intentional flexibility where needed


Note: This was intended to be a discussion, but discussions could not be created due to permissions issues. This issue was created as a fallback.

AI generated by Typist - Go Type Analysis

  • expires on Feb 19, 2026, 11:25 AM UTC

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions