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.
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
├─→ 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:
- 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: