# 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"