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: '
Loading...
' + + - id: "TPL-007" + name: "Implement empty state when no data" + severity: "warning" + description: | + Show empty state when lists have no items + pattern: + file_pattern: "app/templates/**/*.html" + recommended_pattern: '' + +# ============================================================================ +# FRONTEND STYLING RULES +# ============================================================================ + +styling_rules: + + - id: "CSS-001" + name: "Use Tailwind utility classes" + severity: "warning" + description: | + Prefer Tailwind utility classes over custom CSS + pattern: + file_pattern: "app/templates/**/*.html" + encouraged: true + + - id: "CSS-002" + name: "Support dark mode with dark: prefix" + severity: "warning" + description: | + All color classes should include dark mode variants + pattern: + file_pattern: "app/templates/**/*.html" + recommended_pattern: 'class="bg-white dark:bg-gray-800"' + + - id: "CSS-003" + name: "Shop templates use vendor theme CSS variables" + severity: "error" + description: | + Shop templates must use CSS variables for vendor-specific theming + pattern: + file_pattern: "app/templates/shop/**/*.html" + required_pattern: 'var\\(--color-primary\\)' + + - id: "CSS-004" + name: "Mobile-first responsive design" + severity: "warning" + description: | + Use mobile-first responsive classes + pattern: + file_pattern: "app/templates/**/*.html" + recommended_pattern: 'class="grid-cols-1 md:grid-cols-2 lg:grid-cols-4"' + +# ============================================================================ +# MIDDLEWARE RULES +# ============================================================================ + +middleware_rules: + + - id: "MDW-001" + name: "Middleware files use simple nouns without _middleware suffix" + severity: "warning" + description: | + Middleware files should be named with simple nouns (auth.py, not auth_middleware.py) + pattern: + file_pattern: "middleware/**/*.py" + check: "middleware_naming" + + - id: "MDW-002" + name: "Vendor context must be injected for vendor/shop routes" + severity: "error" + description: | + Vendor context middleware must set request.state.vendor_id and request.state.vendor + pattern: + file_pattern: "middleware/vendor_context.py" + required: true + +# ============================================================================ +# MULTI-TENANCY RULES +# ============================================================================ + +multi_tenancy_rules: + + - id: "MT-001" + name: "All queries must be scoped to vendor_id" + severity: "error" + description: | + In vendor/shop contexts, all database queries must filter by vendor_id + pattern: + file_pattern: "app/services/**/*.py" + context: "vendor_shop" + required_pattern: ".filter\\(.*vendor_id.*\\)" + + - id: "MT-002" + name: "No cross-vendor data access" + severity: "error" + description: | + Queries must never access data from other vendors + pattern: + file_pattern: "app/services/**/*.py" + enforcement: "database_query_level" + +# ============================================================================ +# AUTHENTICATION & AUTHORIZATION RULES +# ============================================================================ + +auth_rules: + + - id: "AUTH-001" + name: "Use JWT tokens in Authorization header" + severity: "error" + description: | + Authentication must use JWT tokens in Authorization: Bearer header + pattern: + file_pattern: "app/api/**/*.py" + enforcement: "middleware" + + - id: "AUTH-002" + name: "Role-based access control with Depends" + severity: "error" + description: | + Use Depends(get_current_admin/vendor/customer) for role checks + pattern: + file_pattern: "app/api/v1/**/*.py" + required: "Depends\\(get_current_" + + - id: "AUTH-003" + name: "Never store plain passwords" + severity: "error" + description: | + Always hash passwords with bcrypt before storing + pattern: + file_pattern: "app/services/auth_service.py" + required: "bcrypt" + +# ============================================================================ +# CODE QUALITY RULES +# ============================================================================ + +code_quality_rules: + + - id: "QUAL-001" + name: "All code must be formatted with Ruff" + severity: "error" + description: | + Run 'make format' before committing + enforcement: "pre_commit" + + - id: "QUAL-002" + name: "All code must pass Ruff linting" + severity: "error" + description: | + Run 'make lint' before committing + enforcement: "pre_commit" + + - id: "QUAL-003" + name: "Type hints recommended for functions" + severity: "warning" + description: | + Add type hints to function parameters and return types + pattern: + file_pattern: "app/**/*.py" + encouraged: true + # ============================================================================ # VALIDATION SEVERITY LEVELS # ============================================================================ @@ -382,14 +640,14 @@ severity_levels: warning: description: "Pattern deviation - should be fixed" - exit_code: 0 # Don't fail build, but report + exit_code: 0 info: description: "Suggestion for improvement" exit_code: 0 # ============================================================================ -# IGNORED PATTERNS (False Positives) +# IGNORED PATTERNS # ============================================================================ ignore: @@ -398,14 +656,32 @@ ignore: - "**/test_*.py" - "**/__pycache__/**" - "**/migrations/**" + - "**/alembic/versions/**" - "**/node_modules/**" - "**/.venv/**" - "**/venv/**" - ".venv/**" - "venv/**" + - "**/build/**" + - "**/dist/**" patterns: # Allow HTTPException in specific files - file: "app/core/exceptions.py" pattern: "HTTPException" reason: "Exception handling utilities" + + - file: "app/exceptions/handler.py" + pattern: "HTTPException" + reason: "Exception handler converts to HTTP" + +# ============================================================================ +# DOCUMENTATION +# ============================================================================ + +documentation: + architecture: "docs/architecture/overview.md" + backend: "docs/backend/overview.md" + frontend: "docs/frontend/overview.md" + contributing: "docs/development/contributing.md" + code_quality: "docs/development/code-quality.md"