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
15 KiB
Architecture Overview
This document describes the design principles, architecture patterns, and implementation details of Grokkit.
Table of Contents
- Design Philosophy
- Project Structure
- Core Components
- Data Flow
- Testing Strategy
- Design Patterns
- Future Considerations
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 managementinternal/: Core business logic (not importable outside project)internal/errors/: Domain-specific error typesinternal/git/: Git command abstractioninternal/grok/: API client with streaming supportinternal/linter/: Language detection and linter executioninternal/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):
- CLI flags (
--model grok-4) - Environment variables (
XAI_API_KEY) - Config file (
~/.config/grokkit/config.toml) - 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:
- Marshal request → JSON
- Create HTTP request with context
- Set headers (Authorization, Content-Type)
- Send request
- Stream response line-by-line
- Parse SSE chunks
- Build full response
- 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 troubleshootingINFO: Normal operationsWARN: Potential issuesERROR: 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:
- Keep packages small and focused
- Write tests for pure functions
- Log all external operations
- Use interfaces for testability
- Handle errors explicitly
- Document non-obvious behavior
See also: