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.
594 lines
15 KiB
Markdown
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)
|