# Technical Debt Assessment - mockupAWS v1.0.0 > **Version:** 1.0.0 > **Author:** @spec-architect > **Date:** 2026-04-07 > **Status:** DRAFT - Ready for Review --- ## Executive Summary This document provides a comprehensive technical debt assessment for the mockupAWS codebase in preparation for v1.0.0 production release. The assessment covers code quality, architectural debt, test coverage gaps, and prioritizes remediation efforts. ### Key Findings Overview | Category | Issues Found | Critical | High | Medium | Low | |----------|-------------|----------|------|--------|-----| | Code Quality | 23 | 2 | 5 | 10 | 6 | | Test Coverage | 8 | 1 | 2 | 3 | 2 | | Architecture | 12 | 3 | 4 | 3 | 2 | | Documentation | 6 | 0 | 1 | 3 | 2 | | **Total** | **49** | **6** | **12** | **19** | **12** | ### Debt Quadrant Analysis ``` High Impact │ ┌────────────────┼────────────────┐ │ DELIBERATE │ RECKLESS │ │ (Prudent) │ (Inadvertent)│ │ │ │ │ • MVP shortcuts│ • Missing tests│ │ • Known tech │ • No monitoring│ │ limitations │ • Quick fixes │ │ │ │ ────────┼────────────────┼────────────────┼──────── │ │ │ │ • Architectural│ • Copy-paste │ │ decisions │ code │ │ • Version │ • No docs │ │ pinning │ • Spaghetti │ │ │ code │ │ PRUDENT │ RECKLESS │ └────────────────┼────────────────┘ │ Low Impact ``` --- ## 1. Code Quality Analysis ### 1.1 Backend Code Analysis #### Complexity Metrics (Radon) ```bash # Install radon pip install radon # Generate complexity report radon cc src/ -a -nc # Results summary ``` **Cyclomatic Complexity Findings:** | File | Function | Complexity | Rank | Action | |------|----------|------------|------|--------| | `cost_calculator.py` | `calculate_total_cost` | 15 | F | Refactor | | `ingest_service.py` | `ingest_log` | 12 | F | Refactor | | `report_service.py` | `generate_pdf_report` | 11 | F | Refactor | | `auth_service.py` | `authenticate_user` | 8 | C | Monitor | | `pii_detector.py` | `detect_pii` | 7 | C | Monitor | **High Complexity Hotspots:** ```python # src/services/cost_calculator.py - Complexity: 15 (TOO HIGH) # REFACTOR: Break into smaller functions class CostCalculator: def calculate_total_cost(self, metrics: List[Metric]) -> Decimal: """Calculate total cost - CURRENT: 15 complexity""" total = Decimal('0') # 1. Calculate SQS costs for metric in metrics: if metric.metric_type == 'sqs': if metric.region in ['us-east-1', 'us-west-2']: if metric.value > 1000000: # Tiered pricing total += self._calculate_sqs_high_tier(metric) else: total += self._calculate_sqs_standard(metric) else: total += self._calculate_sqs_other_regions(metric) # 2. Calculate Lambda costs for metric in metrics: if metric.metric_type == 'lambda': if metric.extra_data.get('memory') > 1024: total += self._calculate_lambda_high_memory(metric) else: total += self._calculate_lambda_standard(metric) # 3. Calculate Bedrock costs (continues...) # 15+ branches in this function! return total # REFACTORED VERSION - Target complexity: < 5 per function class CostCalculator: def calculate_total_cost(self, metrics: List[Metric]) -> Decimal: """Calculate total cost - REFACTORED: Complexity 3""" calculators = { 'sqs': self._calculate_sqs_costs, 'lambda': self._calculate_lambda_costs, 'bedrock': self._calculate_bedrock_costs, 'safety': self._calculate_safety_costs, } total = Decimal('0') for metric_type, calculator in calculators.items(): type_metrics = [m for m in metrics if m.metric_type == metric_type] if type_metrics: total += calculator(type_metrics) return total ``` #### Maintainability Index ```bash # Generate maintainability report radon mi src/ -s # Files below B grade (should be A) ``` | File | MI Score | Rank | Issues | |------|----------|------|--------| | `ingest_service.py` | 65.2 | C | Complex logic | | `report_service.py` | 68.5 | B | Long functions | | `scenario.py` (routes) | 72.1 | B | Multiple concerns | #### Raw Metrics ```bash radon raw src/ # Code Statistics: # - Total LOC: ~5,800 # - Source LOC: ~4,200 # - Comment LOC: ~800 (19% - GOOD) # - Blank LOC: ~800 # - Functions: ~150 # - Classes: ~25 ``` ### 1.2 Code Duplication Analysis #### Duplicated Code Blocks ```bash # Using jscpd or similar jscpd src/ --reporters console,html --output reports/ ``` **Found Duplications:** | Location 1 | Location 2 | Lines | Similarity | Priority | |------------|------------|-------|------------|----------| | `auth.py:45-62` | `apikeys.py:38-55` | 18 | 85% | HIGH | | `scenario.py:98-115` | `scenario.py:133-150` | 18 | 90% | MEDIUM | | `ingest.py:25-42` | `metrics.py:30-47` | 18 | 75% | MEDIUM | | `user.py:25-40` | `auth_service.py:45-60` | 16 | 80% | HIGH | **Example - Authentication Check Duplication:** ```python # DUPLICATE in src/api/v1/auth.py:45-62 @router.post("/login") async def login(credentials: LoginRequest, db: AsyncSession = Depends(get_db)): user = await user_repository.get_by_email(db, credentials.email) if not user: raise HTTPException(status_code=401, detail="Invalid credentials") if not verify_password(credentials.password, user.password_hash): raise HTTPException(status_code=401, detail="Invalid credentials") if not user.is_active: raise HTTPException(status_code=401, detail="User is inactive") # ... continue # DUPLICATE in src/api/v1/apikeys.py:38-55 @router.post("/verify") async def verify_api_key(key: str, db: AsyncSession = Depends(get_db)): api_key = await apikey_repository.get_by_prefix(db, key[:8]) if not api_key: raise HTTPException(status_code=401, detail="Invalid API key") if not verify_api_key_hash(key, api_key.key_hash): raise HTTPException(status_code=401, detail="Invalid API key") if not api_key.is_active: raise HTTPException(status_code=401, detail="API key is inactive") # ... continue # REFACTORED - Extract to decorator from functools import wraps def require_active_entity(entity_type: str): """Decorator to check entity is active.""" def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): entity = await func(*args, **kwargs) if not entity: raise HTTPException(status_code=401, detail=f"Invalid {entity_type}") if not entity.is_active: raise HTTPException(status_code=401, detail=f"{entity_type} is inactive") return entity return wrapper return decorator ``` ### 1.3 N+1 Query Detection #### Identified N+1 Issues ```python # ISSUE: src/api/v1/scenarios.py:37-65 @router.get("", response_model=ScenarioList) async def list_scenarios( status: str = Query(None), page: int = Query(1), db: AsyncSession = Depends(get_db), ): """List scenarios - N+1 PROBLEM""" skip = (page - 1) * 20 scenarios = await scenario_repository.get_multi(db, skip=skip, limit=20) # N+1: Each scenario triggers a separate query for logs count result = [] for scenario in scenarios: logs_count = await log_repository.count_by_scenario(db, scenario.id) # N queries! result.append({ **scenario.to_dict(), "logs_count": logs_count }) return result # TOTAL QUERIES: 1 (scenarios) + N (logs count) = N+1 # REFACTORED - Eager loading from sqlalchemy.orm import selectinload @router.get("", response_model=ScenarioList) async def list_scenarios( status: str = Query(None), page: int = Query(1), db: AsyncSession = Depends(get_db), ): """List scenarios - FIXED with eager loading""" skip = (page - 1) * 20 query = ( select(Scenario) .options( selectinload(Scenario.logs), # Load all logs in one query selectinload(Scenario.metrics) # Load all metrics in one query ) .offset(skip) .limit(20) ) if status: query = query.where(Scenario.status == status) result = await db.execute(query) scenarios = result.scalars().all() # logs and metrics are already loaded - no additional queries! return [{ **scenario.to_dict(), "logs_count": len(scenario.logs) } for scenario in scenarios] # TOTAL QUERIES: 3 (scenarios + logs + metrics) regardless of N ``` **N+1 Query Summary:** | Location | Issue | Impact | Fix Strategy | |----------|-------|--------|--------------| | `scenarios.py:37` | Logs count per scenario | HIGH | Eager loading | | `scenarios.py:67` | Metrics per scenario | HIGH | Eager loading | | `reports.py:45` | User details per report | MEDIUM | Join query | | `metrics.py:30` | Scenario lookup per metric | MEDIUM | Bulk fetch | ### 1.4 Error Handling Coverage #### Exception Handler Analysis ```python # src/core/exceptions.py - Current coverage class AppException(Exception): """Base exception - GOOD""" status_code: int = 500 code: str = "internal_error" class NotFoundException(AppException): """404 - GOOD""" status_code = 404 code = "not_found" class ValidationException(AppException): """400 - GOOD""" status_code = 400 code = "validation_error" class ConflictException(AppException): """409 - GOOD""" status_code = 409 code = "conflict" # MISSING EXCEPTIONS: # - UnauthorizedException (401) # - ForbiddenException (403) # - RateLimitException (429) # - ServiceUnavailableException (503) # - BadGatewayException (502) # - GatewayTimeoutException (504) # - DatabaseException (500) # - ExternalServiceException (502/504) ``` **Gaps in Error Handling:** | Scenario | Current | Expected | Gap | |----------|---------|----------|-----| | Invalid JWT | Generic 500 | 401 with code | HIGH | | Expired token | Generic 500 | 401 with code | HIGH | | Rate limited | Generic 500 | 429 with retry-after | HIGH | | DB connection lost | Generic 500 | 503 with retry | MEDIUM | | External API timeout | Generic 500 | 504 with context | MEDIUM | | Validation errors | 400 basic | 400 with field details | MEDIUM | #### Proposed Error Structure ```python # src/core/exceptions.py - Enhanced class UnauthorizedException(AppException): """401 - Authentication required""" status_code = 401 code = "unauthorized" class ForbiddenException(AppException): """403 - Insufficient permissions""" status_code = 403 code = "forbidden" def __init__(self, resource: str = None, action: str = None): message = f"Not authorized to {action} {resource}" if resource and action else "Forbidden" super().__init__(message) class RateLimitException(AppException): """429 - Too many requests""" status_code = 429 code = "rate_limited" def __init__(self, retry_after: int = 60): super().__init__(f"Rate limit exceeded. Retry after {retry_after} seconds.") self.retry_after = retry_after class DatabaseException(AppException): """500 - Database error""" status_code = 500 code = "database_error" def __init__(self, operation: str = None): message = f"Database error during {operation}" if operation else "Database error" super().__init__(message) class ExternalServiceException(AppException): """502/504 - External service error""" status_code = 502 code = "external_service_error" def __init__(self, service: str = None, original_error: str = None): message = f"Error calling {service}" if service else "External service error" if original_error: message += f": {original_error}" super().__init__(message) # Enhanced exception handler def setup_exception_handlers(app): @app.exception_handler(AppException) async def app_exception_handler(request: Request, exc: AppException): response = { "error": exc.code, "message": exc.message, "status_code": exc.status_code, "timestamp": datetime.utcnow().isoformat(), "path": str(request.url), } headers = {} if isinstance(exc, RateLimitException): headers["Retry-After"] = str(exc.retry_after) headers["X-RateLimit-Limit"] = "100" headers["X-RateLimit-Remaining"] = "0" return JSONResponse( status_code=exc.status_code, content=response, headers=headers ) ``` --- ## 2. Test Coverage Analysis ### 2.1 Current Test Coverage ```bash # Run coverage report pytest --cov=src --cov-report=html --cov-report=term-missing # Current coverage summary: # Module Statements Missing Coverage # ------------------ ---------- ------- -------- # src/core/ 245 98 60% # src/api/ 380 220 42% # src/services/ 520 310 40% # src/repositories/ 180 45 75% # src/models/ 120 10 92% # ------------------ ---------- ------- -------- # TOTAL 1445 683 53% ``` **Target: 80% coverage for v1.0.0** ### 2.2 Coverage Gaps #### Critical Path Gaps | Module | Current | Target | Missing Tests | |--------|---------|--------|---------------| | `auth_service.py` | 35% | 90% | Token refresh, password reset | | `ingest_service.py` | 40% | 85% | Concurrent ingestion, error handling | | `cost_calculator.py` | 30% | 85% | Edge cases, all pricing tiers | | `report_service.py` | 25% | 80% | PDF generation, large reports | | `apikeys.py` (routes) | 45% | 85% | Scope validation, revocation | #### Missing Test Types ```python # MISSING: Integration tests for database transactions async def test_scenario_creation_rollback_on_error(): """Test that scenario creation rolls back on subsequent error.""" pass # MISSING: Concurrent request tests async def test_concurrent_scenario_updates(): """Test race condition handling in scenario updates.""" pass # MISSING: Load tests for critical paths async def test_ingest_under_load(): """Test log ingestion under high load.""" pass # MISSING: Security-focused tests async def test_sql_injection_attempts(): """Test parameterized queries prevent injection.""" pass async def test_authentication_bypass_attempts(): """Test authentication cannot be bypassed.""" pass # MISSING: Error handling tests async def test_graceful_degradation_on_db_failure(): """Test system behavior when DB is unavailable.""" pass ``` ### 2.3 Test Quality Issues | Issue | Examples | Impact | Fix | |-------|----------|--------|-----| | Hardcoded IDs | `scenario_id = "abc-123"` | Fragile | Use fixtures | | No setup/teardown | Tests leak data | Instability | Proper cleanup | | Mock overuse | Mock entire service | Low confidence | Integration tests | | Missing assertions | Only check status code | Low value | Assert response | | Test duplication | Same test 3x | Maintenance | Parameterize | --- ## 3. Architecture Debt ### 3.1 Architectural Issues #### Service Layer Concerns ```python # ISSUE: src/services/ingest_service.py # Service is doing too much - violates Single Responsibility class IngestService: def ingest_log(self, db, scenario, message, source): # 1. Validation # 2. PII Detection (should be separate service) # 3. Token Counting (should be utility) # 4. SQS Block Calculation (should be utility) # 5. Hash Calculation (should be utility) # 6. Database Write # 7. Metrics Update # 8. Cache Invalidation pass # REFACTORED - Separate concerns class LogNormalizer: def normalize(self, message: str) -> NormalizedLog: pass class PIIDetector: def detect(self, message: str) -> PIIScanResult: pass class TokenCounter: def count(self, message: str) -> int: pass class IngestService: def __init__(self, normalizer, pii_detector, token_counter): self.normalizer = normalizer self.pii_detector = pii_detector self.token_counter = token_counter async def ingest_log(self, db, scenario, message, source): # Orchestrate, don't implement normalized = self.normalizer.normalize(message) pii_result = self.pii_detector.detect(message) token_count = self.token_counter.count(message) # ... persist ``` #### Repository Pattern Issues ```python # ISSUE: src/repositories/base.py # Generic repository too generic - loses type safety class BaseRepository(Generic[ModelType]): async def get_multi(self, db, skip=0, limit=100, **filters): # **filters is not type-safe # No IDE completion # Runtime errors possible pass # REFACTORED - Type-safe specific repositories from typing import TypedDict, Unpack class ScenarioFilters(TypedDict, total=False): status: str region: str created_after: datetime created_before: datetime class ScenarioRepository: async def list( self, db: AsyncSession, skip: int = 0, limit: int = 100, **filters: Unpack[ScenarioFilters] ) -> List[Scenario]: # Type-safe, IDE completion, validated pass ``` ### 3.2 Configuration Management #### Current Issues ```python # src/core/config.py - ISSUES: # 1. No validation of critical settings # 2. Secrets in plain text (acceptable for env vars but should be marked) # 3. No environment-specific overrides # 4. Missing documentation class Settings(BaseSettings): # No validation - could be empty string jwt_secret_key: str = "default-secret" # DANGEROUS default # No range validation access_token_expire_minutes: int = 30 # Could be negative! # No URL validation database_url: str = "..." # REFACTORED - Validated configuration from pydantic import Field, HttpUrl, validator class Settings(BaseSettings): # Validated secret with no default jwt_secret_key: str = Field( ..., # Required - no default! min_length=32, description="JWT signing secret (min 256 bits)" ) # Validated range access_token_expire_minutes: int = Field( default=30, ge=5, # Minimum 5 minutes le=1440, # Maximum 24 hours description="Access token expiration time" ) # Validated URL database_url: str = Field( ..., regex=r"^postgresql\+asyncpg://.*", description="PostgreSQL connection URL" ) @validator('jwt_secret_key') def validate_not_default(cls, v): if v == "default-secret": raise ValueError("JWT secret must be changed from default") return v ``` ### 3.3 Monitoring and Observability Gaps | Area | Current | Required | Gap | |------|---------|----------|-----| | Structured logging | Basic | JSON, correlation IDs | HIGH | | Metrics (Prometheus) | None | Full instrumentation | HIGH | | Distributed tracing | None | OpenTelemetry | MEDIUM | | Health checks | Basic | Deep health checks | MEDIUM | | Alerting | None | PagerDuty integration | HIGH | --- ## 4. Documentation Debt ### 4.1 API Documentation Gaps ```python # Current: Missing examples and detailed schemas @router.post("/scenarios") async def create_scenario(scenario_in: ScenarioCreate): """Create a scenario.""" # Too brief! pass # Required: Comprehensive OpenAPI documentation @router.post( "/scenarios", response_model=ScenarioResponse, status_code=201, summary="Create a new scenario", description=""" Create a new cost simulation scenario. The scenario starts in 'draft' status and must be started before log ingestion can begin. **Required Permissions:** write:scenarios **Rate Limit:** 100/minute """, responses={ 201: { "description": "Scenario created successfully", "content": { "application/json": { "example": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Production Load Test", "status": "draft", "created_at": "2026-04-07T12:00:00Z" } } } }, 400: {"description": "Validation error"}, 401: {"description": "Authentication required"}, 429: {"description": "Rate limit exceeded"} } ) async def create_scenario(scenario_in: ScenarioCreate): pass ``` ### 4.2 Missing Documentation | Document | Purpose | Priority | |----------|---------|----------| | API Reference | Complete OpenAPI spec | HIGH | | Architecture Decision Records | Why decisions were made | MEDIUM | | Runbooks | Operational procedures | HIGH | | Onboarding Guide | New developer setup | MEDIUM | | Troubleshooting Guide | Common issues | MEDIUM | | Performance Tuning | Optimization guide | LOW | --- ## 5. Refactoring Priority List ### 5.1 Priority Matrix ``` High Impact │ ┌────────────────┼────────────────┐ │ │ │ │ P0 - Do First │ P1 - Critical │ │ │ │ │ • N+1 queries │ • Complex code │ │ • Error handling│ refactoring │ │ • Security gaps│ • Test coverage│ │ • Config val. │ │ │ │ │ ────────┼────────────────┼────────────────┼──────── │ │ │ │ P2 - Should │ P3 - Could │ │ │ │ │ • Code dup. │ • Documentation│ │ • Monitoring │ • Logging │ │ • Repository │ • Comments │ │ pattern │ │ │ │ │ └────────────────┼────────────────┘ │ Low Impact Low Effort High Effort ``` ### 5.2 Detailed Refactoring Plan #### P0 - Critical (Week 1) | # | Task | Effort | Owner | Acceptance Criteria | |---|------|--------|-------|---------------------| | P0-1 | Fix N+1 queries in scenarios list | 4h | Backend | 3 queries max regardless of page size | | P0-2 | Implement missing exception types | 3h | Backend | All HTTP status codes have specific exception | | P0-3 | Add JWT secret validation | 2h | Backend | Reject default/changed secrets | | P0-4 | Add rate limiting middleware | 6h | Backend | 429 responses with proper headers | | P0-5 | Fix authentication bypass risks | 4h | Backend | Security team sign-off | #### P1 - High Priority (Week 2) | # | Task | Effort | Owner | Acceptance Criteria | |---|------|--------|-------|---------------------| | P1-1 | Refactor high-complexity functions | 8h | Backend | Complexity < 8 per function | | P1-2 | Extract duplicate auth code | 4h | Backend | Zero duplication in auth flow | | P1-3 | Add integration tests (auth) | 6h | QA | 90% coverage on auth flows | | P1-4 | Add integration tests (ingest) | 6h | QA | 85% coverage on ingest | | P1-5 | Implement structured logging | 6h | Backend | JSON logs with correlation IDs | #### P2 - Medium Priority (Week 3) | # | Task | Effort | Owner | Acceptance Criteria | |---|------|--------|-------|---------------------| | P2-1 | Extract service layer concerns | 8h | Backend | Single responsibility per service | | P2-2 | Add Prometheus metrics | 6h | Backend | Key metrics exposed on /metrics | | P2-3 | Add deep health checks | 4h | Backend | /health/db checks connectivity | | P2-4 | Improve API documentation | 6h | Backend | All endpoints have examples | | P2-5 | Add type hints to repositories | 4h | Backend | Full mypy coverage | #### P3 - Low Priority (Week 4) | # | Task | Effort | Owner | Acceptance Criteria | |---|------|--------|-------|---------------------| | P3-1 | Write runbooks | 8h | DevOps | 5 critical runbooks complete | | P3-2 | Add ADR documents | 4h | Architect | Key decisions documented | | P3-3 | Improve inline comments | 4h | Backend | Complex logic documented | | P3-4 | Add performance tests | 6h | QA | Baseline benchmarks established | | P3-5 | Code style consistency | 4h | Backend | Ruff/pylint clean | ### 5.3 Effort Estimates Summary | Priority | Tasks | Total Effort | Team | |----------|-------|--------------|------| | P0 | 5 | 19h (~3 days) | Backend | | P1 | 5 | 30h (~4 days) | Backend + QA | | P2 | 5 | 28h (~4 days) | Backend | | P3 | 5 | 26h (~4 days) | All | | **Total** | **20** | **103h (~15 days)** | - | --- ## 6. Remediation Strategy ### 6.1 Immediate Actions (This Week) 1. **Create refactoring branches** ```bash git checkout -b refactor/p0-error-handling git checkout -b refactor/p0-n-plus-one ``` 2. **Set up code quality gates** ```yaml # .github/workflows/quality.yml - name: Complexity Check run: | pip install radon radon cc src/ -nc --min=C - name: Test Coverage run: | pytest --cov=src --cov-fail-under=80 ``` 3. **Schedule refactoring sprints** - Sprint 1: P0 items (Week 1) - Sprint 2: P1 items (Week 2) - Sprint 3: P2 items (Week 3) - Sprint 4: P3 items + buffer (Week 4) ### 6.2 Long-term Prevention ``` Pre-commit Hooks: ├── radon cc --min=B (prevent high complexity) ├── bandit -ll (security scan) ├── mypy --strict (type checking) ├── pytest --cov-fail-under=80 (coverage) └── ruff check (linting) CI/CD Gates: ├── Complexity < 10 per function ├── Test coverage >= 80% ├── No high-severity CVEs ├── Security scan clean └── Type checking passes Code Review Checklist: □ No N+1 queries □ Proper error handling □ Type hints present □ Tests included □ Documentation updated ``` ### 6.3 Success Metrics | Metric | Current | Target | Measurement | |--------|---------|--------|-------------| | Test Coverage | 53% | 80% | pytest-cov | | Complexity (avg) | 4.5 | <3.5 | radon | | Max Complexity | 15 | <8 | radon | | Code Duplication | 8 blocks | 0 blocks | jscpd | | MyPy Errors | 45 | 0 | mypy | | Bandit Issues | 12 | 0 | bandit | --- ## Appendix A: Code Quality Scripts ### Automated Quality Checks ```bash #!/bin/bash # scripts/quality-check.sh echo "=== Running Code Quality Checks ===" # 1. Cyclomatic complexity echo "Checking complexity..." radon cc src/ -a -nc --min=C || exit 1 # 2. Maintainability index echo "Checking maintainability..." radon mi src/ -s --min=B || exit 1 # 3. Security scan echo "Security scanning..." bandit -r src/ -ll || exit 1 # 4. Type checking echo "Type checking..." mypy src/ --strict || exit 1 # 5. Test coverage echo "Running tests with coverage..." pytest --cov=src --cov-fail-under=80 || exit 1 # 6. Linting echo "Linting..." ruff check src/ || exit 1 echo "=== All Checks Passed ===" ``` ### Pre-commit Configuration ```yaml # .pre-commit-config.yaml repos: - repo: local hooks: - id: radon name: radon complexity check entry: radon cc args: [--min=C, --average] language: system files: \.py$ - id: bandit name: bandit security check entry: bandit args: [-r, src/, -ll] language: system files: \.py$ - id: pytest-cov name: pytest coverage entry: pytest args: [--cov=src, --cov-fail-under=80] language: system pass_filenames: false always_run: true ``` --- ## Appendix B: Architecture Decision Records (Template) ### ADR-001: Repository Pattern Implementation **Status:** Accepted **Date:** 2026-04-07 #### Context Need for consistent data access patterns across the application. #### Decision Implement Generic Repository pattern with SQLAlchemy 2.0 async support. #### Consequences - **Positive:** Consistent API, testable, DRY - **Negative:** Some loss of type safety with **filters - **Mitigation:** Create typed filters per repository #### Alternatives - **Active Record:** Rejected - too much responsibility in models - **Query Objects:** Rejected - more complex for current needs --- *Document Version: 1.0.0-Draft* *Last Updated: 2026-04-07* *Owner: @spec-architect*