grokkit/docs/ARCHITECTURE.md
Greg Gauthier 9f1309ba1a feat(lint): add lint command with AI-powered fixes
Introduce new `grokkit lint` command for automatic language detection,
linting, and AI-suggested fixes. Supports 9 languages including Go, Python,
JavaScript, TypeScript, Rust, Ruby, Java, C/C++, and Shell.

- Add cmd/lint.go for command implementation
- Create internal/linter package with detection and execution logic
- Update README.md with usage examples and workflows
- Enhance docs/ARCHITECTURE.md and docs/TROUBLESHOOTING.md
- Add comprehensive tests for linter functionality
2026-03-01 13:21:44 +00:00

15 KiB

Architecture Overview

This document describes the design principles, architecture patterns, and implementation details of Grokkit.

Table of Contents

Design Philosophy

Grokkit follows these core principles:

1. Simplicity First

  • Minimal dependencies (stdlib + well-known libs)
  • Minimal interface surface (CLI commands over TUI interactions)
  • Clear, readable code over clever solutions
  • Single responsibility per package

2. Safety by Default

  • File backups before any modification
  • Confirmation prompts for destructive actions
  • Comprehensive error handling

3. Observability

  • Structured logging for all operations
  • Request/response timing
  • Contextual error messages

4. Testability

  • Interface-based design
  • Dependency injection where needed
  • Unit tests for pure functions

5. User Experience

  • Streaming responses for immediate feedback
  • Persistent history
  • Shell completions

Project Structure

grokkit/
├── main.go                    # Entry point
├── cmd/                       # CLI commands (Cobra)
│   ├── root.go               # Root command + flags
│   ├── chat.go               # Chat command
│   ├── edit.go               # Edit command
│   ├── review.go             # Review command
│   ├── commit.go             # Commit commands
│   ├── completion.go         # Shell completions
│   └── *_test.go             # Command tests
├── config/                    # Configuration (Viper)
│   ├── config.go             # Config loading + getters
│   └── config_test.go        # Config tests
├── internal/                  # Private packages
│   ├── errors/               # Custom error types
│   │   ├── errors.go
│   │   └── errors_test.go
│   ├── git/                  # Git operations wrapper
│   │   ├── git.go
│   │   ├── interface.go      # GitRunner interface
│   │   └── git_test.go
│   ├── grok/                 # Grok API client
│   │   ├── client.go         # HTTP client + streaming
│   │   ├── interface.go      # AIClient interface
│   │   └── client_test.go
│   ├── linter/               # Multi-language linting
│   │   ├── linter.go         # Language detection + linter execution
│   │   └── linter_test.go
│   └── logger/               # Structured logging (slog)
│       ├── logger.go         # Logger setup + helpers
│       └── logger_test.go
├── docs/                      # Documentation
│   ├── TROUBLESHOOTING.md
│   ├── ARCHITECTURE.md       # This file
│   └── CONFIGURATION.md
├── .gitea/workflows/         # CI/CD
│   ├── ci.yml               # Test + lint + build
│   └── release.yml          # Multi-platform releases
└── Makefile                  # Build automation

Package Organization

  • cmd/: Cobra commands, minimal logic (orchestration only)
  • config/: Viper integration, configuration management
  • internal/: Core business logic (not importable outside project)
  • internal/errors/: Domain-specific error types
  • internal/git/: Git command abstraction
  • internal/grok/: API client with streaming support
  • internal/linter/: Language detection and linter execution
  • internal/logger/: Structured logging facade

Core Components

1. CLI Layer (cmd/)

Responsibility: Command parsing, user interaction, orchestration

Pattern: Command pattern (Cobra)

var chatCmd = &cobra.Command{
    Use:   "chat",
    Short: "Interactive chat",
    Run: func(cmd *cobra.Command, args []string) {
        // 1. Parse flags
        // 2. Load config
        // 3. Call business logic
        // 4. Handle errors
        // 5. Display output
    },
}

Key characteristics:

  • Thin orchestration layer
  • No business logic
  • Delegates to internal packages
  • Handles user I/O only

2. Configuration Layer (config/)

Responsibility: Load and manage configuration from multiple sources

Pattern: Singleton (via Viper)

// Priority: CLI flag > env var > config file > default
func GetModel(flagModel string) string {
    if flagModel != "" {
        return resolveAlias(flagModel)
    }
    return viper.GetString("default_model")
}

Configuration sources (in order):

  1. CLI flags (--model grok-4)
  2. Environment variables (XAI_API_KEY)
  3. Config file (~/.config/grokkit/config.toml)
  4. Defaults (hardcoded in config.Load())

3. API Client (internal/grok/)

Responsibility: HTTP communication with Grok API

Pattern: Client pattern with streaming support

type Client struct {
    APIKey  string
    BaseURL string
}

// Public API
func (c *Client) Stream(messages, model) string
func (c *Client) StreamSilent(messages, model) string
func (c *Client) StreamWithTemp(messages, model, temp) string

Key features:

  • Context-aware requests (60s timeout)
  • Server-Sent Events (SSE) streaming
  • Silent vs live output modes
  • Comprehensive logging

Request flow:

  1. Marshal request → JSON
  2. Create HTTP request with context
  3. Set headers (Authorization, Content-Type)
  4. Send request
  5. Stream response line-by-line
  6. Parse SSE chunks
  7. Build full response
  8. Log metrics

4. Git Integration (internal/git/)

Responsibility: Abstract git command execution

Pattern: Command wrapper + Interface

type GitRunner interface {
    Run(args []string) (string, error)
    IsRepo() bool
}

// Implementation
func Run(args []string) (string, error) {
    out, err := exec.Command("git", args...).Output()
    // Log execution
    // Wrap errors
    return string(out), err
}

Why wrapped:

  • Logging of all git operations
  • Consistent error handling
  • Testability via interface
  • Future: git library instead of exec

5. Linter (internal/linter/)

Responsibility: Language detection and linter orchestration

Pattern: Strategy pattern (multiple linters per language)

type Language struct {
    Name       string
    Extensions []string
    Linters    []Linter  // Multiple options per language
}

type Linter struct {
    Name        string
    Command     string
    Args        []string
    InstallInfo string
}

// Auto-detect language and find available linter
func LintFile(filePath string) (*LintResult, error) {
    lang := DetectLanguage(filePath)        // By extension
    linter := FindAvailableLinter(lang)     // Check PATH
    return RunLinter(filePath, linter)      // Execute
}

Supported languages:

  • Go (golangci-lint, go vet)
  • Python (pylint, flake8, ruff)
  • JavaScript/TypeScript (eslint, tsc)
  • Rust (clippy)
  • Ruby (rubocop)
  • Java (checkstyle)
  • C/C++ (clang-tidy)
  • Shell (shellcheck)

Design features:

  • Fallback linters (tries multiple options)
  • Tool availability checking
  • Helpful install instructions
  • Exit code interpretation

6. Logging (internal/logger/)

Responsibility: Structured logging with context

Pattern: Facade over stdlib log/slog

// Structured logging with key-value pairs
logger.Info("API request completed",
    "model", model,
    "duration_ms", duration,
    "response_length", length)

Features:

  • JSON output format
  • Dynamic log levels
  • Multi-writer (file + stderr in debug)
  • Context enrichment

Log levels:

  • DEBUG: Detailed info for troubleshooting
  • INFO: Normal operations
  • WARN: Potential issues
  • ERROR: Failures requiring attention

6. Error Handling (internal/errors/)

Responsibility: Domain-specific error types

Pattern: Custom error types with context

type GitError struct {
    Command string
    Err     error
}

type APIError struct {
    StatusCode int
    Message    string
    Err        error
}

Benefits:

  • Type-safe error handling
  • Rich error context
  • Error wrapping (errors.Is, errors.As)

Data Flow

Chat Command Flow

User Input
    ↓
chatCmd.Run()
    ↓
config.GetModel()           # Resolve model
    ↓
loadChatHistory()           # Load previous messages
    ↓
grok.Client.Stream()        # API call
    ├─→ logger.Info()       # Log request
    ├─→ HTTP POST           # Send to API
    ├─→ Stream chunks       # Receive SSE
    └─→ logger.Info()       # Log completion
    ↓
saveChatHistory()           # Persist conversation
    ↓
Display to user

Edit Command Flow

User: grokkit edit file.go "instruction"
    ↓
editCmd.Run()
    ├─→ Validate file exists
    ├─→ Read original content
    ├─→ Create backup (.bak)
    ├─→ Clean "Last modified" comments
    ↓
grok.Client.StreamSilent()  # Get AI response
    ├─→ [Same as chat, but silent]
    ↓
grok.CleanCodeResponse()    # Remove markdown fences
    ↓
Display preview             # Show diff-style output
    ↓
Prompt user (y/n)
    ↓
Write file (if confirmed)
    ├─→ logger.Info()       # Log changes
    └─→ Keep backup

Commit Command Flow

User: grokkit commit
    ↓
commitCmd.Run()
    ↓
git.Run(["diff", "--cached"])   # Get staged changes
    ├─→ logger.Debug()           # Log git command
    ↓
grok.Client.Stream()             # Generate commit message
    ↓
Display message + prompt
    ↓
exec.Command("git", "commit")    # Execute commit
    ├─→ logger.Info()            # Log result

Lint Command Flow

User: grokkit lint file.py [--dry-run] [--auto-fix]
    ↓
lintCmd.Run()
    ├─→ Validate file exists
    ↓
linter.LintFile()
    ├─→ DetectLanguage()         # By file extension
    ├─→ FindAvailableLinter()    # Check PATH
    ├─→ RunLinter()              # Execute linter
    ↓
Display linter output
    ↓
(if --dry-run) → Exit
    ↓
(if issues found) → Request AI fixes
    ↓
grok.Client.StreamSilent()       # Get fixed code
    ├─→ Build prompt with linter output + original code
    ├─→ Stream API response
    ↓
grok.CleanCodeResponse()         # Remove markdown
    ↓
Create backup (.bak)
    ↓
(if not --auto-fix) → Show preview + prompt
    ↓
Write fixed file
    ↓
linter.LintFile()                # Verify fixes
    ├─→ Display verification result

Testing Strategy

Unit Tests

Coverage goals:

  • Internal packages: >70%
  • Helper functions: 100%
  • Commands: Test helpers, not Cobra execution

Approach:

  • Table-driven tests
  • Isolated environments (temp directories)
  • No external dependencies in unit tests

Example:

func TestRemoveLastModifiedComments(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        expected string
    }{
        {"removes comment", "// Last modified\ncode", "code"},
        {"preserves other", "// Comment\ncode", "// Comment\ncode"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := removeLastModifiedComments(tt.input)
            assert.Equal(t, tt.expected, result)
        })
    }
}

What's NOT tested (and why)

Cobra command execution - Requires CLI integration tests User input (Scanln) - Requires terminal mocking API streaming - Would need mock HTTP server os.Exit() paths - Can't test process termination

These require integration/E2E tests, which are a different testing layer.

Test Organization

package/
├── implementation.go      # Production code
└── implementation_test.go # Tests (same package)

Benefits:

  • Tests can access private functions
  • Clear mapping between code and tests
  • Go convention

Design Patterns

1. Interface Segregation

// Small, focused interfaces
type AIClient interface {
    Stream(messages []map[string]string, model string) string
    StreamSilent(messages []map[string]string, model string) string
}

type GitRunner interface {
    Run(args []string) (string, error)
    IsRepo() bool
}

Benefits:

  • Easy to mock for testing
  • Flexibility to swap implementations
  • Clear contracts

2. Facade Pattern (Logger)

// Simple API hides slog complexity
logger.Info("message", "key", "value")
logger.Error("error", "context", ctx)

3. Command Pattern (CLI)

// Each command is independent
var chatCmd = &cobra.Command{...}
var editCmd = &cobra.Command{...}

4. Strategy Pattern (Streaming modes)

// Different behaviors via parameters
func streamInternal(messages, model, temp, printLive bool) {
    if printLive {
        fmt.Print(content)  // Live streaming
    }
    // Silent streaming builds full response
}

5. Template Method (Git operations)

// Base wrapper with logging
func Run(args []string) (string, error) {
    logger.Debug("executing", "command", args)
    out, err := exec.Command("git", args...).Output()
    if err != nil {
        logger.Error("failed", "command", args, "error", err)
    }
    return string(out), err
}

Future Considerations

Scalability

Current limitations:

  • Single API key (no multi-user support)
  • No request queuing
  • No rate limiting client-side
  • Chat history grows unbounded

Future improvements:

  • Rate limit handling with backoff
  • History trimming/rotation
  • Concurrent request support
  • Request caching

Extensibility

Easy to add:

  • New commands (add to cmd/)
  • New models (config aliases)
  • New output formats (change display logic)
  • New logging destinations (slog handlers)

Harder to add:

  • Multiple AI providers (requires abstraction)
  • Non-streaming APIs (requires client refactor)
  • GUI (CLI-focused architecture)

Performance

Current bottlenecks:

  • Network latency (streaming helps with perception)
  • API response time (model-dependent)
  • File I/O for large history (rare)

Optimization opportunities:

  • Request caching (for repeated queries)
  • Parallel git operations (if multiple files)
  • Lazy loading of history (if very large)

Security

Current measures:

  • API key via environment variable
  • No credential storage
  • Backup files for safety

Future considerations:

  • Encrypted config storage
  • API key rotation support
  • Audit logging for file changes
  • Sandboxed file operations

Contributing

When contributing, maintain these principles:

  1. Keep packages small and focused
  2. Write tests for pure functions
  3. Log all external operations
  4. Use interfaces for testability
  5. Handle errors explicitly
  6. Document non-obvious behavior

See also: