# DPSCoach Architecture
## Overview
DPSCoach is a multi-surface application (CLI, MCP tools, desktop UI) built around a **contract-first** architecture. The core analyzer (`server.py` + `dps_logs/`) produces deterministic JSON payloads that all consumers (UI, MCP clients, CLI) interpret identically.
---
## Component Layers
### 1. **Data Layer**: Combat Log Parser
**Files**: `dps_logs/parser.py`, `dps_logs/metrics.py`
**Responsibilities**:
- Stream-parse UTF-8 `.txt`/`.log` files into `(run_id, events)` tuples
- Compute per-run DPS, DPM, crit%, heavy%, skill breakdowns
- Aggregate multi-run summaries with top skills by damage
**Key Constraints**:
- **Streaming**: Uses `yield` to avoid loading entire logs into memory
- **Defensive**: Skips rows with unexpected column counts (no crash)
- **Rounding**: All metrics rounded to 3 decimals for validation parity
**Public API**:
```python
from dps_logs.parser import load_runs
from dps_logs.metrics import summarize_run, build_summary
runs = [summarize_run(rid, events) for rid, events in load_runs(path, limit_runs=10)]
summary = build_summary(runs)
```
---
### 2. **Service Layer**: CLI & MCP Server
**Files**: `server.py`, `mcp_server.py`, `dps_logs/event_store.py`
**Responsibilities**:
- **CLI** (`server.py`): Analyze logs, output JSON/Markdown to stdout or files
- **MCP Server** (`mcp_server.py`): Expose tools (`analyze_dps_logs`, `query_dps`, `get_analysis_packet`) via FastMCP
- **DuckDB Store** (`event_store.py`): Load events into in-memory table, execute safe SELECT queries
**Key Constraints**:
- **Contract stability**: `analyze_logs()` returns `{generated_at, source, runs, summary}` consumed by UI, MCP, and CLI
- **Read-only safety**: `query_dps` tool enforces SELECT-only via AST parse
- **Parameter clamping**: `last_n_runs` limited to [1, 50], `bucket_seconds` restricted to allowed values
**Public API**:
```python
from server import analyze_logs
payload = analyze_logs(log_dir="path/to/logs", limit_runs=5)
# Returns: {"generated_at": str, "source": {...}, "runs": [...], "summary": {...}}
```
---
### 3. **Application Layer**: Desktop UI & MCP Client
**Files**: `app/main.py`, `app/mcp_client.py`, `app/coach_local.py`
**Responsibilities**:
- **Desktop UI** (`main.py`): PySide6 app with tabbed views (Summary, Runs, Skills, Coach)
- **MCP Client** (`mcp_client.py`): Subprocess wrapper for `python -m mcp_server`, stdio JSON-RPC
- **SQLCoach** (`coach_local.py`): Intent detection → deterministic handlers or LLM-planned SQL
**Key Constraints**:
- **Background threads**: `QThread` workers for analysis/coach to keep UI responsive
- **Model integrity**: GGUF validation (SHA-256, size, self-test) before enabling chat
- **Graceful degradation**: Missing model → download prompt; malformed LLM output → fallback query
**Public API**:
```python
from app.mcp_client import MCPAnalyzerClient
client = MCPAnalyzerClient(log_dir="path/to/logs", limit_runs=5)
payload = client.get_analysis() # Same shape as server.analyze_logs
```
---
## Data Flow
### CLI Workflow
```
User runs:
python -m server --input-path <path> --limit-runs 5 --pretty
1. server.py calls analyze_logs(log_dir, limit_runs=5)
2. parser.load_runs yields (run_id, events)
3. metrics.summarize_run computes per-run stats
4. metrics.build_summary merges runs
5. server.py prints JSON to stdout
```
### MCP Tool Workflow
```
MCP client invokes:
analyze_dps_logs(log_dir=<path>, limit_runs=5)
1. mcp_server.py calls server.analyze_logs internally
2. Returns same JSON payload to client
3. Client (e.g., Claude Desktop) displays results
```
### Desktop UI Workflow
```
User clicks "Analyze Logs":
1. main.py spawns AnalysisWorker (QThread)
2. Worker calls MCPAnalyzerClient.get_analysis()
3. MCPClient spawns `python -m mcp_server` subprocess
4. Subprocess executes analyze_dps_logs tool
5. Worker emits analysis_complete signal
6. UI updates Summary/Runs/Skills tables
```
### Coach Workflow
```
User asks "Why did my crit rate drop?":
1. main.py spawns CoachWorker (QThread)
2. Worker calls coach_local.ask(question, analysis_payload)
3. Coach detects intent (CRIT_BUCKET_TREND)
4. Coach routes to deterministic handler or plans SQL
5. If SQL planned, coach calls MCPClient.query_dps(sql)
6. Coach formats answer with trace
7. Worker emits coach_response signal
8. UI appends answer to transcript
```
---
## Key Design Decisions
### 1. **Why MCP as the Contract Boundary?**
- **Portability**: CLI, desktop UI, and third-party MCP clients (e.g., Claude Desktop) consume identical payloads
- **Testability**: Smoke tests verify MCP tool output matches CLI output
- **Future-proofing**: Easy to add web API or VS Code extension later
### 2. **Why DuckDB Instead of Pandas?**
- **Safety**: SQL queries are parsed and validated (SELECT-only)
- **Performance**: Columnar storage, efficient aggregations for large logs
- **Flexibility**: Ad-hoc queries without shipping raw events to UI
### 3. **Why Intent Routing Instead of Pure LLM?**
- **Determinism**: 90% of questions (RUNS, SKILLS, CRIT_BUCKET_TREND) skip the LLM entirely
- **Consistency**: Deterministic handlers guarantee identical formatting for repeated questions
- **Observability**: SQL trace shows exactly what was queried, no black-box LLM magic
### 4. **Why Subprocess MCP Client Instead of Direct Import?**
- **Isolation**: MCP server runs in a child process, protecting UI from crashes
- **Reusability**: Same MCP server binary used by CLI, UI, and third-party clients
- **Upgradability**: Can swap MCP server implementation without recompiling UI
---
## Extension Points
### Adding a New Metric
1. **Update parser** (`dps_logs/metrics.py`): Add field to `summarize_run` return dict
2. **Update summary** (`build_summary`): Merge new metric across runs
3. **Update UI** (`app/main.py`): Add column to Summary/Runs table
4. **Update tests** (`tests/`): Add assertions for new metric
### Adding a New Coach Intent
1. **Define intent** (`app/coach_local.py`): Add to `COACH_INTENTS` enum
2. **Add route handler**: Implement `_route_<intent>` method
3. **Update prompt**: Add example to `COACH_SYSTEM_PROMPT`
4. **Add test** (`tests/test_coach_intents.py`): Verify detection and handler
### Adding a New MCP Tool
1. **Define tool** (`mcp_server.py`): Add FastMCP `@mcp.tool()` function
2. **Update client** (`app/mcp_client.py`): Add wrapper method
3. **Update coach** (`app/coach_local.py`): Add to `AVAILABLE_TOOLS` prompt section
4. **Add smoke test** (`tests/smoke_mcp_tool.py`): Verify tool response
---
## Testing Strategy
### Unit Tests (73 tests)
- **Intent detection**: `tests/test_coach_intents.py` verifies prompt → intent mapping
- **Route handlers**: Each `_route_*` method has 3-5 tests (valid input, edge cases, bounds)
- **DPS bounds**: `test_runs_analysis_dps_not_exceeding_bound` ensures DPS ≤ total_damage / duration
- **Session persistence**: `test_session_remembers_analysis` verifies `ask` reuses cached analysis
### Integration Tests
- **MCP parity** (`tests/smoke_mcp_tool.py`): MCP tool output matches CLI output
- **Packaged smoke** (`server.py --smoke-packaged`): DuckDB queries work in bundled build
### Validation
- **Math validation** (`validate_output.py`): Recomputes DPS from `total_damage / duration_seconds`, fails if deviation > 1e-3
- **Model self-test** (`app/model_manager.py`): GGUF model must respond "OK" to trivial prompt
---
## Security Considerations
### 1. **SQL Injection Prevention**
- User inputs quoted via `.replace("'", "''")` before interpolation
- `query_dps` tool parses SQL with AST, rejects INSERT/UPDATE/DELETE
### 2. **File System Isolation**
- MCP server never writes files (output to stdout only)
- UI resolves paths with `Path(...).expanduser()` to prevent `..` traversal
### 3. **Model Integrity**
- GGUF files validated via SHA-256 hash and minimum size check
- Corrupt models rejected before llama.cpp loads them
### 4. **Subprocess Safety**
- No `shell=True` anywhere in codebase
- MCP client spawns `python -m mcp_server` directly (no command injection)
---
## Performance Characteristics
### Typical Workload
- **Log parsing**: 100 MB log file with 50 runs → ~2-3 seconds (streaming)
- **DuckDB query**: 10K events → <100ms for aggregations (crit%, skill breakdown)
- **LLM inference**: Qwen2.5-7B Q4_K_M → 500-1000ms per coach answer (CPU)
- **UI responsiveness**: All long-running tasks on `QThread`, UI never blocks
### Bottlenecks
- **Model inference**: CPU-bound, single-threaded (llama.cpp)
- **Log parsing**: I/O-bound for very large files (>500 MB)
### Optimizations
- **Streaming parser**: `yield` per run avoids loading entire log into RAM
- **DuckDB columnar storage**: 10x faster aggregations vs. pandas for >10K rows
- **Background threads**: Model download, analysis, coach inference never block UI
---
## Deployment
### Development
```powershell
pip install -r requirements.txt
pip install -r app/requirements_coach.txt
python -m app.main
```
### Packaged Build
```powershell
# See PACKAGING.md for full instructions
python -m PyInstaller app/pyinstaller.spec
# Output: dist/DPSCoach.exe (standalone, no Python install required)
```
### MCP Server (for Claude Desktop)
```json
{
"mcpServers": {
"tl-dps-mcp": {
"command": "python",
"args": ["-m", "mcp_server"],
"cwd": "C:\\path\\to\\repo"
}
}
}
```
---
## Future Enhancements
- **Web API**: Expose analyze_logs as REST endpoint (FastAPI)
- **SQLite on-disk**: Cache timeline buckets for sessions >1000 runs
- **Multi-model support**: Allow users to swap GGUF models (Mistral, Llama3, etc.)
- **Cloud sync**: Optional upload of analysis results to cloud storage
- **Telemetry**: Anonymous usage stats (with user opt-in) to guide feature development