grokkit/docs/ARCHITECTURE.md
Greg Gauthier 81fd65b14d
All checks were successful
CI / Test (push) Successful in 35s
CI / Lint (push) Successful in 25s
CI / Build (push) Successful in 19s
refactor(cmd): remove automatic .bak backup creation
Eliminate .bak file backups from edit, docs, lint, testgen, and agent commands to simplify safety features, relying on previews and confirmations instead. Update README, architecture docs, troubleshooting, and TODOs to reflect changes. Adjust tests to remove backup assertions.
2026-03-03 20:44:39 +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
    ├─→ 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

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
    ↓
(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: