grokkit/docs/ARCHITECTURE.md

598 lines
15 KiB
Markdown
Raw Normal View History

# Architecture Overview
This document describes the design principles, architecture patterns, and implementation details of Grokkit.
## Table of Contents
- [Design Philosophy](#design-philosophy)
- [Project Structure](#project-structure)
- [Core Components](#core-components)
- [Data Flow](#data-flow)
- [Testing Strategy](#testing-strategy)
- [Design Patterns](#design-patterns)
- [Future Considerations](#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 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)
```go
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)
```go
// 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
```go
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
```go
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)
```go
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`
```go
// 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
```go
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:**
```go
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
```go
// 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)
```go
// Simple API hides slog complexity
logger.Info("message", "key", "value")
logger.Error("error", "context", ctx)
```
### 3. Command Pattern (CLI)
```go
// Each command is independent
var chatCmd = &cobra.Command{...}
var editCmd = &cobra.Command{...}
```
### 4. Strategy Pattern (Streaming modes)
```go
// 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)
```go
// 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:**
- [Configuration Guide](CONFIGURATION.md)
- [Troubleshooting](TROUBLESHOOTING.md)