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>
408 lines
14 KiB
YAML
408 lines
14 KiB
YAML
# 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"
|