# Architecture Rules Reference This document provides a comprehensive reference for all architectural rules enforced by the `scripts/validate_architecture.py` validator. ## Overview The architecture validator ensures consistent patterns, separation of concerns, and best practices across the entire codebase. Rules are organized by category and severity level. **Version:** 2.0 **Total Rules:** 50+ **Configuration File:** `.architecture-rules.yaml` ## Running the Validator ### Using Make Commands (Recommended) ```bash # Check all files make arch-check # Check a single file make arch-check-file file="app/api/v1/admin/vendors.py" # Check all files related to an entity (company, vendor, user, etc.) make arch-check-object name="company" make arch-check-object name="vendor" # Full QA (includes arch-check) make qa ``` ### Using Python Directly ```bash # Check all files python scripts/validate_architecture.py # Check specific directory python scripts/validate_architecture.py -d app/api/ # Check a single file python scripts/validate_architecture.py -f app/api/v1/admin/vendors.py # Check all files for an entity python scripts/validate_architecture.py -o company python scripts/validate_architecture.py -o vendor # Verbose output python scripts/validate_architecture.py --verbose # JSON output (for CI/CD) python scripts/validate_architecture.py --json ``` ### Output Format The validator displays a summary table for validated files: ``` 📋 FILE SUMMARY: -------------------------------------------------------------------------------- File Status Errors Warnings ----------------------------- -------- ------- -------- app/api/v1/admin/companies.py ❌ FAILED 6 9 app/services/company_service.py ✅ PASSED 0 0 models/database/company.py ✅ PASSED 0 0 -------------------------------------------------------------------------------- Total: 3 files | ✅ 2 passed | ❌ 1 failed | 6 errors | 9 warnings ``` ## Severity Levels | Severity | Description | Exit Code | Action Required | |----------|-------------|-----------|-----------------| | **Error** | Critical architectural violation | 1 | Must fix before committing | | **Warning** | Pattern deviation | 0 | Should fix, doesn't block | | **Info** | Suggestion for improvement | 0 | Optional improvement | --- ## Core Architectural Principles 1. **Separation of Concerns** - API endpoints handle HTTP, services handle business logic 2. **Layered Architecture** - Routes → Services → Models 3. **Type Safety** - Pydantic for API, SQLAlchemy for database 4. **Proper Exception Handling** - Domain exceptions in services, HTTPException in routes 5. **Multi-Tenancy** - All queries scoped to vendor_id 6. **Consistent Naming** - API files: plural, Services: singular+service, Models: singular --- ## Backend Rules ### API Endpoint Rules (app/api/v1/**/*.py) #### API-001: Use Pydantic Models for Request/Response **Severity:** Error All API endpoints must use Pydantic models (BaseModel) for request bodies and response models. Never use raw dicts or SQLAlchemy models directly. ```python # ✅ Good @router.post("/vendors", response_model=VendorResponse) async def create_vendor(vendor: VendorCreate): return vendor_service.create_vendor(db, vendor) # ❌ Bad @router.post("/vendors") async def create_vendor(data: dict): return {"name": data["name"]} # No validation! ``` #### API-002: No Business Logic in Endpoints **Severity:** Error API endpoints should only handle HTTP concerns (validation, auth, response formatting). All business logic must be delegated to service layer. ```python # ✅ Good @router.post("/vendors") async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)): result = vendor_service.create_vendor(db, vendor) return result # ❌ Bad @router.post("/vendors") async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)): db_vendor = Vendor(name=vendor.name) db.add(db_vendor) # Business logic in endpoint! db.commit() return db_vendor ``` **Anti-patterns detected:** - `db.add(` - `db.commit()` - `db.query(` #### API-003: No HTTPException in Endpoints **Severity:** Error API endpoints must NOT raise HTTPException directly. Instead, let domain exceptions bubble up to the global exception handler which converts them to appropriate HTTP responses. ```python # ✅ Good - Let domain exceptions bubble up @router.post("/vendors") async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)): # Service raises VendorAlreadyExistsException if duplicate # Global handler converts to 409 Conflict return vendor_service.create_vendor(db, vendor) # ❌ Bad - Don't raise HTTPException directly @router.post("/vendors") async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)): if vendor_service.exists(db, vendor.subdomain): raise HTTPException(status_code=409, detail="Vendor exists") # BAD! return vendor_service.create_vendor(db, vendor) ``` **Pattern:** Services raise domain exceptions → Global handler converts to HTTP responses #### API-004: Proper Authentication **Severity:** Warning Protected endpoints must use Depends() for authentication. ```python # ✅ Good - Protected endpoint with authentication @router.post("/vendors") async def create_vendor( vendor: VendorCreate, current_user: User = Depends(get_current_admin), db: Session = Depends(get_db) ): pass ``` **Auto-Excluded Files:** The validator automatically skips API-004 checks for authentication endpoint files (`*/auth.py`) since login, logout, and registration endpoints are intentionally public. **Marking Public Endpoints:** For other intentionally public endpoints (webhooks, health checks, etc.), use a comment marker: ```python # ✅ Good - Webhook endpoint marked as public # public - Stripe webhook receives external callbacks @router.post("/webhook/stripe") def stripe_webhook(request: Request): ... # ✅ Good - Using noqa style @router.post("/health") # noqa: API-004 def health_check(): ... ``` **Recognized markers:** - `# public` - descriptive marker for intentionally unauthenticated endpoints - `# noqa: API-004` - standard noqa style to suppress the warning #### API-005: Multi-Tenant Scoping **Severity:** Error All queries in vendor/shop contexts must filter by vendor_id. --- ### Service Layer Rules (app/services/**/*.py) #### SVC-001: No HTTPException in Services **Severity:** Error Services are business logic layer - they should NOT know about HTTP. Raise domain-specific exceptions instead. ```python # ✅ 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") # ❌ 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! ``` #### SVC-002: Use Proper Exception Handling **Severity:** Error Services should raise meaningful domain exceptions, not generic Exception. ```python # ✅ Good class VendorAlreadyExistsError(Exception): pass def create_vendor(self, db: Session, vendor_data): if self._vendor_exists(db, vendor_data.subdomain): raise VendorAlreadyExistsError(f"Subdomain {vendor_data.subdomain} taken") # ❌ Bad def create_vendor(self, db: Session, vendor_data): if self._vendor_exists(db, vendor_data.subdomain): raise Exception("Vendor exists") # Too generic! ``` #### SVC-003: Accept DB Session as Parameter **Severity:** Error Service methods should receive database session as a parameter for testability and transaction control. ```python # ✅ Good def create_vendor(self, db: Session, vendor_data: VendorCreate): vendor = Vendor(**vendor_data.dict()) db.add(vendor) db.commit() return vendor # ❌ Bad def create_vendor(self, vendor_data: VendorCreate): db = SessionLocal() # Don't create session inside! vendor = Vendor(**vendor_data.dict()) db.add(vendor) db.commit() ``` #### SVC-004: Use Pydantic Models for Input **Severity:** Warning Service methods should accept Pydantic models for complex inputs to ensure type safety. #### SVC-005: Scope Queries to vendor_id **Severity:** Error All database queries must be scoped to vendor_id to prevent cross-tenant data access. --- ### Model Rules (models/**/*.py) #### MDL-001: Database Models Use SQLAlchemy Base **Severity:** Error All database models must inherit from SQLAlchemy Base. #### MDL-002: Never Mix SQLAlchemy and Pydantic **Severity:** Error ```python # ❌ Bad class Product(Base, BaseModel): # NEVER DO THIS! pass ``` Keep them separate: - `models/database/product.py` - SQLAlchemy models - `models/schema/product.py` - Pydantic models #### MDL-003: Use from_attributes in Pydantic **Severity:** Error Pydantic response models must enable `from_attributes` to work with SQLAlchemy models. ```python # ✅ Good class ProductResponse(BaseModel): id: int name: str class Config: from_attributes = True ``` #### MDL-004: Use Singular Table Names **Severity:** Warning Database table names should be singular lowercase (e.g., 'vendor' not 'vendors'). --- ### Exception Handling Rules #### EXC-001: Define Custom Exceptions **Severity:** Warning Create domain-specific exceptions in `app/exceptions/` for better error handling. ```python # ✅ Good - app/exceptions/vendor.py class VendorError(Exception): """Base exception for vendor-related errors""" pass class VendorNotFoundError(VendorError): pass class VendorAlreadyExistsError(VendorError): pass ``` #### EXC-002: Never Use Bare Except **Severity:** Error ```python # ❌ Bad try: result = service.do_something() except: # Too broad! pass # ✅ 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}") ``` #### EXC-003: Log Exceptions with Context **Severity:** Warning When catching exceptions, log them with context and stack trace. ```python # ✅ Good try: result = service.process() except Exception as e: logger.error(f"Processing failed: {e}", exc_info=True) ``` --- ### Naming Convention Rules #### NAM-001: API Files Use PLURAL Names **Severity:** Error ``` ✅ app/api/v1/admin/vendors.py ✅ app/api/v1/admin/products.py ❌ app/api/v1/admin/vendor.py ``` Exceptions: `__init__.py`, `auth.py`, `health.py` #### NAM-002: Service Files Use SINGULAR + service **Severity:** Error ``` ✅ app/services/vendor_service.py ✅ app/services/product_service.py ❌ app/services/vendors_service.py ``` #### NAM-003: Model Files Use SINGULAR Names **Severity:** Error ``` ✅ models/database/product.py ✅ models/schema/vendor.py ❌ models/database/products.py ``` #### NAM-004: Use 'vendor' not 'shop' **Severity:** Warning Use consistent terminology: 'vendor' for shop owners, 'shop' only for customer-facing frontend. ```python # ✅ Good vendor_id vendor_service # ❌ Bad shop_id shop_service ``` #### NAM-005: Use 'inventory' not 'stock' **Severity:** Warning ```python # ✅ Good inventory_service inventory_level # ❌ Bad stock_service stock_level ``` --- ## Frontend Rules ### JavaScript Rules (static/**/js/**/*.js) #### JS-001: Use Centralized Logger **Severity:** Error Never use console.log, console.error, console.warn directly. Use window.LogConfig.createLogger(). ```javascript // ✅ Good const pageLog = window.LogConfig.createLogger('dashboard'); pageLog.info('Dashboard loaded'); pageLog.error('Failed to load data', error); // ❌ Bad console.log('Dashboard loaded'); console.error('Failed to load data', error); ``` Exceptions: Bootstrap messages with `console.log('✅'` allowed. #### JS-002: Use Lowercase apiClient **Severity:** Error ```javascript // ✅ Good await apiClient.get('/api/v1/vendors'); await apiClient.post('/api/v1/products', data); // ❌ Bad await ApiClient.get('/api/v1/vendors'); await API_CLIENT.post('/api/v1/products', data); ``` #### JS-003: Alpine Components Must Inherit ...data() **Severity:** Error All Alpine.js components must inherit base layout data using spread operator. ```javascript // ✅ Good function adminDashboard() { return { ...data(), // Inherit base layout data currentPage: 'dashboard', // ... component-specific data }; } // ❌ Bad function adminDashboard() { return { currentPage: 'dashboard', // Missing ...data()! }; } ``` #### JS-004: Set currentPage in Components **Severity:** Error All Alpine.js page components must set a currentPage identifier. ```javascript // ✅ Good return { ...data(), currentPage: 'dashboard', // Required! // ... }; ``` #### JS-005: Initialization Guards **Severity:** Error Init methods should prevent duplicate initialization. ```javascript // ✅ Good init() { if (window._pageInitialized) return; window._pageInitialized = true; // Initialization logic... } ``` #### JS-006: Error Handling for Async Operations **Severity:** Error All API calls and async operations must have error handling. ```javascript // ✅ Good async loadData() { try { const response = await apiClient.get('/api/v1/data'); this.items = response.data; } catch (error) { window.LogConfig.logError(error, 'Load Data'); Utils.showToast('Failed to load data', 'error'); } } ``` #### JS-007: Set Loading State **Severity:** Warning Loading state should be set before and cleared after async operations. ```javascript // ✅ Good async loadData() { this.loading = true; try { const response = await apiClient.get('/api/v1/data'); this.items = response.data; } catch (error) { // Error handling } finally { this.loading = false; } } ``` --- ### Template Rules (app/templates/**/*.html) #### TPL-001: Admin Templates Extend admin/base.html **Severity:** Error All admin templates must extend the base template for consistent layout (sidebar, navigation, etc.). ```jinja {# ✅ Good - Extends base template #} {% extends "admin/base.html" %} {% block content %} ... {% endblock %} ``` **Auto-Excluded Files:** The validator automatically skips TPL-001 checks for: - `login.html` - Standalone login page (no sidebar/navigation needed) - `errors/*.html` - Error pages extend `errors/base.html` instead - `test-*.html` - Test/development templates - `base.html` - The base template itself - `partials/*.html` - Partial templates included in other templates **Marking Standalone Templates:** For other templates that intentionally don't extend base.html, use a comment marker in the first 5 lines: ```jinja {# standalone - Minimal monitoring page without admin chrome #} ... ``` **Recognized markers:** - `{# standalone #}` - Jinja comment style - `{# noqa: TPL-001 #}` - Standard noqa style - `` - HTML comment style #### TPL-002: Vendor Templates Extend vendor/base.html **Severity:** Error ```jinja ✅ {% extends "vendor/base.html" %} ``` #### TPL-003: Shop Templates Extend shop/base.html **Severity:** Error ```jinja ✅ {% extends "shop/base.html" %} ``` #### TPL-004: Use x-text for Dynamic Content **Severity:** Warning Use x-text directive for dynamic content to prevent XSS vulnerabilities. ```html ✅
❌{{ item.name }}
``` #### TPL-005: Use x-html ONLY for Safe Content **Severity:** Error Use x-html only for trusted content like icons, never for user-generated content. ```html ✅ ❌ ``` #### TPL-006: Implement Loading State **Severity:** Warning All templates that load data should show loading state. ```html ✅No items found
``` --- ### Jinja Macro Rules (app/templates/**/*.html) #### MAC-001: Use Shared Macros for Repeated Patterns **Severity:** Error Never copy-paste HTML patterns. Use or create shared macros in `app/templates/shared/macros/`. ```jinja {# ✅ Good - Using shared macro #} {% from 'shared/macros/tables.html' import table_wrapper, table_header %} {% call table_wrapper() %} {{ table_header(['Name', 'Email', 'Status']) }} ... {% endcall %} {# ❌ Bad - Copy-pasting table HTML #}