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

594 lines
15 KiB
Markdown

# 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
├─→ 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:**
```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)