Files
orion/.architecture-rules.yaml
Samir Boulahtit 1e720ae0e5 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>
2025-11-28 07:44:51 +01:00

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"