diff --git a/.architecture-rules.yaml b/.architecture-rules.yaml index 480a59ce..ea4308bf 100644 --- a/.architecture-rules.yaml +++ b/.architecture-rules.yaml @@ -2,8 +2,9 @@ # 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" +version: "2.0" project: "letzshop-product-import" +description: "Comprehensive architectural rules for multi-tenant e-commerce platform" # ============================================================================ # CORE ARCHITECTURAL PRINCIPLES @@ -22,6 +23,12 @@ principles: - name: "Proper Exception Handling" description: "Services throw domain exceptions. Routes catch and convert to HTTPException." + - name: "Multi-Tenancy" + description: "All queries must be scoped to vendor_id. No cross-vendor data access." + + - name: "Consistent Naming" + description: "API files: plural, Services: singular+service, Models: singular" + # ============================================================================ # API ENDPOINT RULES (app/api/v1/**/*.py) # ============================================================================ @@ -36,23 +43,14 @@ api_endpoint_rules: 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 + - "return db_object" 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! + async def create_vendor(vendor: VendorCreate): + return vendor_service.create_vendor(db, vendor) - id: "API-002" name: "Endpoint must NOT contain business logic" @@ -66,25 +64,6 @@ api_endpoint_rules: - "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" @@ -94,28 +73,7 @@ api_endpoint_rules: 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 + check: "exception_handling" - id: "API-004" name: "Endpoint must have proper authentication/authorization" @@ -127,14 +85,18 @@ api_endpoint_rules: 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 + + - id: "API-005" + name: "Multi-tenant endpoints must scope queries to vendor_id" + severity: "error" + description: | + All queries in vendor/shop contexts must filter by vendor_id. + Use request.state.vendor_id from middleware. + pattern: + file_pattern: "app/api/v1/vendor/**/*.py" + file_pattern: "app/api/v1/shop/**/*.py" + discouraged_patterns: + - "db.query(.*).all()" # Without vendor filter # ============================================================================ # SERVICE LAYER RULES (app/services/**/*.py) @@ -153,17 +115,6 @@ service_layer_rules: 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" @@ -173,23 +124,8 @@ service_layer_rules: 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! + - "raise Exception\\(" - id: "SVC-003" name: "Service methods must accept db session as parameter" @@ -204,22 +140,6 @@ service_layer_rules: 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" @@ -230,12 +150,19 @@ service_layer_rules: pattern: file_pattern: "app/services/**/*.py" encouraged_patterns: - - "def .+\\(.*: BaseModel" - - "def .+\\(.*: .*Create" - - "def .+\\(.*: .*Update" + - "BaseModel" + + - id: "SVC-005" + name: "Service must scope queries to vendor_id in multi-tenant contexts" + severity: "error" + description: | + All database queries must be scoped to vendor_id to prevent cross-tenant data access. + pattern: + file_pattern: "app/services/**/*.py" + check: "vendor_scoping" # ============================================================================ -# MODEL RULES (app/models/**/*.py) +# MODEL RULES (models/database/*.py, models/schema/*.py) # ============================================================================ model_rules: @@ -247,10 +174,9 @@ model_rules: All database models must inherit from SQLAlchemy Base and use proper column definitions with types and constraints. pattern: - file_pattern: "app/models/**/*.py" + file_pattern: "models/database/**/*.py" required_patterns: - "class.*\\(Base\\):" - - "from.*sqlalchemy.*import.*Column" - id: "MDL-002" name: "Use Pydantic models separately from SQLAlchemy models" @@ -259,9 +185,28 @@ model_rules: Never mix SQLAlchemy and Pydantic in the same model. SQLAlchemy = database schema, Pydantic = API validation/serialization. pattern: - file_pattern: "app/models/**/*.py" + file_pattern: "models/**/*.py" anti_patterns: - - "class.*\\(Base, BaseModel\\):" # Multiple inheritance - BAD! + - "class.*\\(Base, BaseModel\\):" + + - id: "MDL-003" + name: "Pydantic models must use from_attributes for ORM mode" + severity: "error" + description: | + Pydantic response models must enable from_attributes to work with SQLAlchemy models. + pattern: + file_pattern: "models/schema/**/*.py" + required_in_response_models: + - "from_attributes = True" + + - id: "MDL-004" + name: "Database models use singular table names" + severity: "warning" + description: | + Database table names should be singular lowercase (e.g., 'vendor' not 'vendors'). + pattern: + file_pattern: "models/database/**/*.py" + check: "table_naming" # ============================================================================ # EXCEPTION HANDLING RULES @@ -278,17 +223,10 @@ exception_rules: 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" @@ -300,42 +238,90 @@ exception_rules: 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 + + - id: "EXC-003" + name: "Log all exceptions with context" + severity: "warning" + description: | + When catching exceptions, log them with context and stack trace. + pattern: + file_pattern: "app/services/**/*.py" + encouraged_patterns: + - "logger.error" + - "exc_info=True" # ============================================================================ -# JAVASCRIPT ARCHITECTURE RULES +# NAMING CONVENTION RULES +# ============================================================================ + +naming_rules: + + - id: "NAM-001" + name: "API files use PLURAL names" + severity: "error" + description: | + API endpoint files should use plural names (vendors.py, products.py) + pattern: + file_pattern: "app/api/v1/**/*.py" + check: "plural_naming" + exceptions: + - "__init__.py" + - "auth.py" + - "health.py" + + - id: "NAM-002" + name: "Service files use SINGULAR + 'service' suffix" + severity: "error" + description: | + Service files should use singular name + _service (vendor_service.py) + pattern: + file_pattern: "app/services/**/*.py" + check: "service_naming" + + - id: "NAM-003" + name: "Model files use SINGULAR names" + severity: "error" + description: | + Both database and schema model files use singular names (product.py) + pattern: + file_pattern: "models/**/*.py" + check: "singular_naming" + + - id: "NAM-004" + name: "Use consistent terminology: vendor not shop" + severity: "warning" + description: | + Use 'vendor' consistently, not 'shop' (except for shop frontend) + pattern: + file_pattern: "app/**/*.py" + discouraged_terms: + - "shop_id" # Use vendor_id + - "shop_service" # Use vendor_service + + - id: "NAM-005" + name: "Use consistent terminology: inventory not stock" + severity: "warning" + description: | + Use 'inventory' consistently, not 'stock' + pattern: + file_pattern: "app/**/*.py" + discouraged_terms: + - "stock_service" # Use inventory_service + +# ============================================================================ +# JAVASCRIPT ARCHITECTURE RULES (Frontend) # ============================================================================ 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" + severity: "error" + description: | + Use window.LogConfig.createLogger() for consistent logging. + Never use console.log, console.error, console.warn directly. pattern: - file_pattern: "static/admin/js/**/*.js" + file_pattern: "static/**/js/**/*.js" anti_patterns: - "console\\.log" - "console\\.error" @@ -344,23 +330,81 @@ javascript_rules: - "// eslint-disable" - "console.log('✅" # Bootstrap messages allowed + - id: "JS-002" + name: "Use lowercase apiClient for API calls" + severity: "error" + description: | + Use lowercase 'apiClient' consistently, not 'ApiClient' or 'API_CLIENT' + pattern: + file_pattern: "static/**/js/**/*.js" + anti_patterns: + - "ApiClient\\." + - "API_CLIENT\\." + required_pattern: "apiClient\\." + - id: "JS-003" name: "Alpine components must spread ...data()" severity: "error" - description: "All Alpine.js components must inherit base layout data" + description: | + All Alpine.js components must inherit base layout data using spread operator pattern: - file_pattern: "static/admin/js/**/*.js" + file_pattern: "static/**/js/**/*.js" required_in_alpine_components: - "\\.\\.\\.data\\(\\)" + - id: "JS-004" + name: "Alpine components must set currentPage" + severity: "error" + description: | + All Alpine.js page components must set a currentPage identifier + pattern: + file_pattern: "static/**/js/**/*.js" + required_in_alpine_components: + - "currentPage:" + + - id: "JS-005" + name: "Initialization methods must include guard" + severity: "error" + description: | + Init methods should prevent duplicate initialization with guard + pattern: + file_pattern: "static/**/js/**/*.js" + recommended_pattern: | + if (window._pageInitialized) return; + window._pageInitialized = true; + + - id: "JS-006" + name: "All async operations must have try/catch with error logging" + severity: "error" + description: | + All API calls and async operations must have error handling + pattern: + file_pattern: "static/**/js/**/*.js" + check: "async_error_handling" + + - id: "JS-007" + name: "Set loading state before async operations" + severity: "warning" + description: | + Loading state should be set before and cleared after async operations + pattern: + file_pattern: "static/**/js/**/*.js" + recommended_pattern: | + loading = true; + try { + // operation + } finally { + loading = false; + } + # ============================================================================ -# TEMPLATE RULES +# TEMPLATE RULES (Jinja2) # ============================================================================ template_rules: - id: "TPL-001" - name: "Admin templates must extend base.html" + name: "Admin templates must extend admin/base.html" severity: "error" description: "All admin templates must extend the base template for consistency" pattern: @@ -371,6 +415,220 @@ template_rules: - "base.html" - "partials/" + - id: "TPL-002" + name: "Vendor templates must extend vendor/base.html" + severity: "error" + description: "All vendor templates must extend the base template" + pattern: + file_pattern: "app/templates/vendor/**/*.html" + required_patterns: + - "{% extends ['\"]vendor/base\\.html['\"] %}" + exceptions: + - "base.html" + - "partials/" + + - id: "TPL-003" + name: "Shop templates must extend shop/base.html" + severity: "error" + description: "All shop templates must extend the base template" + pattern: + file_pattern: "app/templates/shop/**/*.html" + required_patterns: + - "{% extends ['\"]shop/base\\.html['\"] %}" + exceptions: + - "base.html" + - "partials/" + + - id: "TPL-004" + name: "Use x-text for dynamic text content (prevents XSS)" + severity: "warning" + description: | + Use x-text directive for dynamic content to prevent XSS vulnerabilities + pattern: + file_pattern: "app/templates/**/*.html" + recommended_pattern: '
' + + - id: "TPL-005" + name: "Use x-html ONLY for safe content" + severity: "error" + description: | + Use x-html only for trusted content like icons, never for user-generated content + pattern: + file_pattern: "app/templates/**/*.html" + safe_usage: + - 'x-html="\\$icon\\(' + + - id: "TPL-006" + name: "Implement loading state for data loads" + severity: "warning" + description: | + All templates that load data should show loading state + pattern: + file_pattern: "app/templates/**/*.html" + recommended_pattern: '