feat: add architecture validation system with comprehensive pattern enforcement
Implemented automated architecture validation to enforce design decisions: Architecture Validation System: - Created .architecture-rules.yaml with comprehensive rule definitions - Implemented validate_architecture.py script with AST-based validation - Added pre-commit hook configuration for automatic validation - Comprehensive documentation in docs/architecture/architecture-patterns.md Key Design Rules Enforced: - API-001 to API-004: API endpoint patterns (Pydantic models, no business logic, exception handling, auth) - SVC-001 to SVC-004: Service layer patterns (domain exceptions, db session params, no HTTP concerns) - MDL-001 to MDL-002: Model separation (SQLAlchemy vs Pydantic) - EXC-001 to EXC-002: Exception handling (custom exceptions, no bare except) - JS-001 to JS-003: JavaScript patterns (apiClient, logger, Alpine components) - TPL-001: Template patterns (extend base.html) Features: - Validates separation of concerns (routes vs services vs models) - Enforces proper exception handling (domain exceptions in services, HTTP in routes) - Checks database session patterns and Pydantic model usage - JavaScript and template validation - Detailed error reporting with suggestions - Integration with pre-commit hooks and CI/CD UI Fix: - Fixed icon names in content-pages.html (pencil→edit, trash→delete) Documentation: - Added architecture patterns guide with examples - Created scripts/README.md for validator usage - Updated mkdocs.yml with architecture documentation - Built and verified documentation successfully Usage: python scripts/validate_architecture.py # Validate all python scripts/validate_architecture.py --verbose # With details python scripts/validate_architecture.py --errors-only # Errors only 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
407
.architecture-rules.yaml
Normal file
407
.architecture-rules.yaml
Normal file
@@ -0,0 +1,407 @@
|
||||
# Architecture Rules Configuration
|
||||
# This file defines the key architectural decisions and patterns that must be followed
|
||||
# across the application. The validator script uses these rules to check compliance.
|
||||
|
||||
version: "1.0"
|
||||
project: "letzshop-product-import"
|
||||
|
||||
# ============================================================================
|
||||
# CORE ARCHITECTURAL PRINCIPLES
|
||||
# ============================================================================
|
||||
|
||||
principles:
|
||||
- name: "Separation of Concerns"
|
||||
description: "API endpoints should only handle HTTP concerns. Business logic belongs in services."
|
||||
|
||||
- name: "Layered Architecture"
|
||||
description: "Routes → Services → Models. Each layer has specific responsibilities."
|
||||
|
||||
- name: "Type Safety"
|
||||
description: "Use Pydantic models for request/response validation. Use SQLAlchemy models for database."
|
||||
|
||||
- name: "Proper Exception Handling"
|
||||
description: "Services throw domain exceptions. Routes catch and convert to HTTPException."
|
||||
|
||||
# ============================================================================
|
||||
# API ENDPOINT RULES (app/api/v1/**/*.py)
|
||||
# ============================================================================
|
||||
|
||||
api_endpoint_rules:
|
||||
|
||||
- id: "API-001"
|
||||
name: "Endpoint must use Pydantic models for request/response"
|
||||
severity: "error"
|
||||
description: |
|
||||
All API endpoints must use Pydantic models (BaseModel) for request bodies
|
||||
and response models. Never use raw dicts or SQLAlchemy models directly.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
check: "pydantic_model_usage"
|
||||
anti_patterns:
|
||||
- "return dict"
|
||||
- "-> dict"
|
||||
- "return db_object" # SQLAlchemy model returned directly
|
||||
example_good: |
|
||||
class VendorCreate(BaseModel):
|
||||
name: str
|
||||
|
||||
@router.post("/vendors", response_model=VendorResponse)
|
||||
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
|
||||
result = vendor_service.create_vendor(db, vendor)
|
||||
return result
|
||||
example_bad: |
|
||||
@router.post("/vendors")
|
||||
async def create_vendor(data: dict, db: Session = Depends(get_db)):
|
||||
return {"name": data["name"]} # No validation!
|
||||
|
||||
- id: "API-002"
|
||||
name: "Endpoint must NOT contain business logic"
|
||||
severity: "error"
|
||||
description: |
|
||||
API endpoints should only handle HTTP concerns (validation, auth, response formatting).
|
||||
All business logic must be delegated to service layer.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
anti_patterns:
|
||||
- "db.add("
|
||||
- "db.commit()"
|
||||
- "db.query("
|
||||
- "SELECT"
|
||||
- "UPDATE"
|
||||
- "DELETE"
|
||||
exceptions:
|
||||
- "db parameter passed to service" # Allowed
|
||||
example_good: |
|
||||
@router.post("/vendors")
|
||||
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
|
||||
# Delegate to service
|
||||
result = vendor_service.create_vendor(db, vendor)
|
||||
return result
|
||||
example_bad: |
|
||||
@router.post("/vendors")
|
||||
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
|
||||
# Business logic in endpoint - BAD!
|
||||
db_vendor = Vendor(name=vendor.name)
|
||||
db.add(db_vendor)
|
||||
db.commit()
|
||||
return db_vendor
|
||||
|
||||
- id: "API-003"
|
||||
name: "Endpoint must catch service exceptions and convert to HTTPException"
|
||||
severity: "error"
|
||||
description: |
|
||||
API endpoints must catch domain exceptions from services and convert them
|
||||
to appropriate HTTPException with proper status codes.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
required_patterns:
|
||||
- "try:"
|
||||
- "except"
|
||||
- "HTTPException"
|
||||
or_pattern: "service_method_without_exception_handling"
|
||||
example_good: |
|
||||
@router.post("/vendors")
|
||||
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
|
||||
try:
|
||||
result = vendor_service.create_vendor(db, vendor)
|
||||
return result
|
||||
except VendorAlreadyExistsError as e:
|
||||
raise HTTPException(status_code=409, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
example_bad: |
|
||||
@router.post("/vendors")
|
||||
async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
|
||||
# No exception handling - service errors leak to client!
|
||||
result = vendor_service.create_vendor(db, vendor)
|
||||
return result
|
||||
|
||||
- id: "API-004"
|
||||
name: "Endpoint must have proper authentication/authorization"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Protected endpoints must use Depends() for authentication.
|
||||
Use get_current_user, get_current_admin, etc.
|
||||
pattern:
|
||||
file_pattern: "app/api/v1/**/*.py"
|
||||
required_if_not_public:
|
||||
- "Depends(get_current_"
|
||||
example_good: |
|
||||
@router.post("/vendors")
|
||||
async def create_vendor(
|
||||
vendor: VendorCreate,
|
||||
current_user: User = Depends(get_current_admin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
pass
|
||||
|
||||
# ============================================================================
|
||||
# SERVICE LAYER RULES (app/services/**/*.py)
|
||||
# ============================================================================
|
||||
|
||||
service_layer_rules:
|
||||
|
||||
- id: "SVC-001"
|
||||
name: "Service must NOT raise HTTPException"
|
||||
severity: "error"
|
||||
description: |
|
||||
Services are business logic layer - they should NOT know about HTTP.
|
||||
Raise domain-specific exceptions instead (ValueError, custom exceptions).
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
anti_patterns:
|
||||
- "raise HTTPException"
|
||||
- "from fastapi import HTTPException"
|
||||
example_good: |
|
||||
class VendorService:
|
||||
def create_vendor(self, db: Session, vendor_data):
|
||||
if self._vendor_exists(db, vendor_data.subdomain):
|
||||
raise VendorAlreadyExistsError(f"Vendor {vendor_data.subdomain} exists")
|
||||
# ... business logic
|
||||
example_bad: |
|
||||
class VendorService:
|
||||
def create_vendor(self, db: Session, vendor_data):
|
||||
if self._vendor_exists(db, vendor_data.subdomain):
|
||||
raise HTTPException(status_code=409, detail="Vendor exists") # BAD!
|
||||
|
||||
- id: "SVC-002"
|
||||
name: "Service must use proper exception handling"
|
||||
severity: "error"
|
||||
description: |
|
||||
Services should raise meaningful domain exceptions, not generic Exception.
|
||||
Create custom exception classes for business rule violations.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
required_patterns:
|
||||
- "class.*Error\\(Exception\\):" # Custom exception classes
|
||||
discouraged_patterns:
|
||||
- "raise Exception\\(" # Too generic
|
||||
example_good: |
|
||||
class VendorAlreadyExistsError(Exception):
|
||||
pass
|
||||
|
||||
class VendorService:
|
||||
def create_vendor(self, db: Session, vendor_data):
|
||||
if self._vendor_exists(db, vendor_data.subdomain):
|
||||
raise VendorAlreadyExistsError(f"Subdomain {vendor_data.subdomain} taken")
|
||||
example_bad: |
|
||||
class VendorService:
|
||||
def create_vendor(self, db: Session, vendor_data):
|
||||
if self._vendor_exists(db, vendor_data.subdomain):
|
||||
raise Exception("Vendor exists") # Too generic!
|
||||
|
||||
- id: "SVC-003"
|
||||
name: "Service methods must accept db session as parameter"
|
||||
severity: "error"
|
||||
description: |
|
||||
Service methods should receive database session as a parameter for testability
|
||||
and transaction control. Never create session inside service.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
required_in_method_signature:
|
||||
- "db: Session"
|
||||
anti_patterns:
|
||||
- "SessionLocal()"
|
||||
- "get_db()"
|
||||
example_good: |
|
||||
class VendorService:
|
||||
def create_vendor(self, db: Session, vendor_data: VendorCreate):
|
||||
# db passed as parameter - testable and transactional
|
||||
vendor = Vendor(**vendor_data.dict())
|
||||
db.add(vendor)
|
||||
db.commit()
|
||||
return vendor
|
||||
example_bad: |
|
||||
class VendorService:
|
||||
def create_vendor(self, vendor_data: VendorCreate):
|
||||
# Creating session inside - BAD!
|
||||
db = SessionLocal()
|
||||
vendor = Vendor(**vendor_data.dict())
|
||||
db.add(vendor)
|
||||
db.commit()
|
||||
|
||||
- id: "SVC-004"
|
||||
name: "Service must use Pydantic models for input validation"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Service methods should accept Pydantic models for complex inputs
|
||||
to ensure type safety and validation.
|
||||
pattern:
|
||||
file_pattern: "app/services/**/*.py"
|
||||
encouraged_patterns:
|
||||
- "def .+\\(.*: BaseModel"
|
||||
- "def .+\\(.*: .*Create"
|
||||
- "def .+\\(.*: .*Update"
|
||||
|
||||
# ============================================================================
|
||||
# MODEL RULES (app/models/**/*.py)
|
||||
# ============================================================================
|
||||
|
||||
model_rules:
|
||||
|
||||
- id: "MDL-001"
|
||||
name: "Database models must use SQLAlchemy Base"
|
||||
severity: "error"
|
||||
description: |
|
||||
All database models must inherit from SQLAlchemy Base and use proper
|
||||
column definitions with types and constraints.
|
||||
pattern:
|
||||
file_pattern: "app/models/**/*.py"
|
||||
required_patterns:
|
||||
- "class.*\\(Base\\):"
|
||||
- "from.*sqlalchemy.*import.*Column"
|
||||
|
||||
- id: "MDL-002"
|
||||
name: "Use Pydantic models separately from SQLAlchemy models"
|
||||
severity: "error"
|
||||
description: |
|
||||
Never mix SQLAlchemy and Pydantic in the same model.
|
||||
SQLAlchemy = database schema, Pydantic = API validation/serialization.
|
||||
pattern:
|
||||
file_pattern: "app/models/**/*.py"
|
||||
anti_patterns:
|
||||
- "class.*\\(Base, BaseModel\\):" # Multiple inheritance - BAD!
|
||||
|
||||
# ============================================================================
|
||||
# EXCEPTION HANDLING RULES
|
||||
# ============================================================================
|
||||
|
||||
exception_rules:
|
||||
|
||||
- id: "EXC-001"
|
||||
name: "Define custom exceptions in exceptions module"
|
||||
severity: "warning"
|
||||
description: |
|
||||
Create domain-specific exceptions in app/exceptions/ for better
|
||||
error handling and clarity.
|
||||
pattern:
|
||||
file_pattern: "app/exceptions/**/*.py"
|
||||
encouraged_structure: |
|
||||
# app/exceptions/vendor_exceptions.py
|
||||
class VendorError(Exception):
|
||||
"""Base exception for vendor-related errors"""
|
||||
pass
|
||||
|
||||
class VendorNotFoundError(VendorError):
|
||||
pass
|
||||
|
||||
class VendorAlreadyExistsError(VendorError):
|
||||
pass
|
||||
|
||||
- id: "EXC-002"
|
||||
name: "Never use bare except"
|
||||
severity: "error"
|
||||
description: |
|
||||
Always specify exception types. Bare except catches everything including
|
||||
KeyboardInterrupt and SystemExit.
|
||||
pattern:
|
||||
file_pattern: "**/*.py"
|
||||
anti_patterns:
|
||||
- "except:"
|
||||
- "except\\s*:"
|
||||
example_good: |
|
||||
try:
|
||||
result = service.do_something()
|
||||
except ValueError as e:
|
||||
logger.error(f"Validation error: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
example_bad: |
|
||||
try:
|
||||
result = service.do_something()
|
||||
except: # BAD! Too broad
|
||||
pass
|
||||
|
||||
# ============================================================================
|
||||
# JAVASCRIPT ARCHITECTURE RULES
|
||||
# ============================================================================
|
||||
|
||||
javascript_rules:
|
||||
|
||||
- id: "JS-001"
|
||||
name: "Use apiClient directly, not window.apiClient"
|
||||
severity: "warning"
|
||||
description: "API client is globally available, no need for window prefix"
|
||||
pattern:
|
||||
file_pattern: "static/admin/js/**/*.js"
|
||||
anti_patterns:
|
||||
- "window\\.apiClient"
|
||||
example_good: "await apiClient.get('/api/v1/vendors')"
|
||||
example_bad: "await window.apiClient.get('/api/v1/vendors')"
|
||||
|
||||
- id: "JS-002"
|
||||
name: "Use centralized logger, not console"
|
||||
severity: "warning"
|
||||
description: "Use window.LogConfig.createLogger() for consistent logging"
|
||||
pattern:
|
||||
file_pattern: "static/admin/js/**/*.js"
|
||||
anti_patterns:
|
||||
- "console\\.log"
|
||||
- "console\\.error"
|
||||
- "console\\.warn"
|
||||
exceptions:
|
||||
- "// eslint-disable"
|
||||
- "console.log('✅" # Bootstrap messages allowed
|
||||
|
||||
- id: "JS-003"
|
||||
name: "Alpine components must spread ...data()"
|
||||
severity: "error"
|
||||
description: "All Alpine.js components must inherit base layout data"
|
||||
pattern:
|
||||
file_pattern: "static/admin/js/**/*.js"
|
||||
required_in_alpine_components:
|
||||
- "\\.\\.\\.data\\(\\)"
|
||||
|
||||
# ============================================================================
|
||||
# TEMPLATE RULES
|
||||
# ============================================================================
|
||||
|
||||
template_rules:
|
||||
|
||||
- id: "TPL-001"
|
||||
name: "Admin templates must extend base.html"
|
||||
severity: "error"
|
||||
description: "All admin templates must extend the base template for consistency"
|
||||
pattern:
|
||||
file_pattern: "app/templates/admin/**/*.html"
|
||||
required_patterns:
|
||||
- "{% extends ['\"]admin/base\\.html['\"] %}"
|
||||
exceptions:
|
||||
- "base.html"
|
||||
- "partials/"
|
||||
|
||||
# ============================================================================
|
||||
# VALIDATION SEVERITY LEVELS
|
||||
# ============================================================================
|
||||
|
||||
severity_levels:
|
||||
error:
|
||||
description: "Critical architectural violation - must be fixed"
|
||||
exit_code: 1
|
||||
|
||||
warning:
|
||||
description: "Pattern deviation - should be fixed"
|
||||
exit_code: 0 # Don't fail build, but report
|
||||
|
||||
info:
|
||||
description: "Suggestion for improvement"
|
||||
exit_code: 0
|
||||
|
||||
# ============================================================================
|
||||
# IGNORED PATTERNS (False Positives)
|
||||
# ============================================================================
|
||||
|
||||
ignore:
|
||||
files:
|
||||
- "**/*_test.py"
|
||||
- "**/test_*.py"
|
||||
- "**/__pycache__/**"
|
||||
- "**/migrations/**"
|
||||
- "**/node_modules/**"
|
||||
|
||||
patterns:
|
||||
# Allow HTTPException in specific files
|
||||
- file: "app/core/exceptions.py"
|
||||
pattern: "HTTPException"
|
||||
reason: "Exception handling utilities"
|
||||
Reference in New Issue
Block a user