# Architecture Rules Reference This document provides a comprehensive reference for all architectural rules enforced by the `scripts/validate/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/stores.py" # Check all files related to an entity (merchant, store, user, etc.) make arch-check-object name="merchant" make arch-check-object name="store" # Full QA (includes arch-check) make qa ``` ### Using Python Directly ```bash # Check all files python scripts/validate/validate_architecture.py # Check specific directory python scripts/validate/validate_architecture.py -d app/api/ # Check a single file python scripts/validate/validate_architecture.py -f app/api/v1/admin/stores.py # Check all files for an entity python scripts/validate/validate_architecture.py -o merchant python scripts/validate/validate_architecture.py -o store # Verbose output python scripts/validate/validate_architecture.py --verbose # JSON output (for CI/CD) python scripts/validate/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/merchants.py ❌ FAILED 6 9 app/services/merchant_service.py ✅ PASSED 0 0 models/database/merchant.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 store_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("/stores", response_model=StoreResponse) async def create_store(store: StoreCreate): return store_service.create_store(db, store) # ❌ Bad @router.post("/stores") async def create_store(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("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): result = store_service.create_store(db, store) return result # ❌ Bad @router.post("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): db_store = Store(name=store.name) db.add(db_store) # Business logic in endpoint! db.commit() return db_store ``` **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("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): # Service raises StoreAlreadyExistsException if duplicate # Global handler converts to 409 Conflict return store_service.create_store(db, store) # ❌ Bad - Don't raise HTTPException directly @router.post("/stores") async def create_store(store: StoreCreate, db: Session = Depends(get_db)): if store_service.exists(db, store.subdomain): raise HTTPException(status_code=409, detail="Store exists") # BAD! return store_service.create_store(db, store) ``` **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("/stores") async def create_store( store: StoreCreate, 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 store/shop contexts must filter by store_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 StoreService: def create_store(self, db: Session, store_data): if self._store_exists(db, store_data.subdomain): raise StoreAlreadyExistsError(f"Store {store_data.subdomain} exists") # ❌ Bad class StoreService: def create_store(self, db: Session, store_data): if self._store_exists(db, store_data.subdomain): raise HTTPException(status_code=409, detail="Store exists") # BAD! ``` #### SVC-002: Use Proper Exception Handling **Severity:** Error Services should raise meaningful domain exceptions, not generic Exception. ```python # ✅ Good class StoreAlreadyExistsError(Exception): pass def create_store(self, db: Session, store_data): if self._store_exists(db, store_data.subdomain): raise StoreAlreadyExistsError(f"Subdomain {store_data.subdomain} taken") # ❌ Bad def create_store(self, db: Session, store_data): if self._store_exists(db, store_data.subdomain): raise Exception("Store 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_store(self, db: Session, store_data: StoreCreate): store = Store(**store_data.dict()) db.add(store) db.commit() return store # ❌ Bad def create_store(self, store_data: StoreCreate): db = SessionLocal() # Don't create session inside! store = Store(**store_data.dict()) db.add(store) 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 store_id **Severity:** Error All database queries must be scoped to store_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., 'store' not 'stores'). --- ### 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/store.py class StoreError(Exception): """Base exception for store-related errors""" pass class StoreNotFoundError(StoreError): pass class StoreAlreadyExistsError(StoreError): 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/stores.py ✅ app/api/v1/admin/products.py ❌ app/api/v1/admin/store.py ``` Exceptions: `__init__.py`, `auth.py`, `health.py` #### NAM-002: Service Files Use SINGULAR + service **Severity:** Error ``` ✅ app/services/store_service.py ✅ app/services/product_service.py ❌ app/services/stores_service.py ``` #### NAM-003: Model Files Use SINGULAR Names **Severity:** Error ``` ✅ models/database/product.py ✅ models/schema/store.py ❌ models/database/products.py ``` #### NAM-004: Use 'store' not 'shop' **Severity:** Warning Use consistent terminology: 'store' for shop owners, 'shop' only for customer-facing frontend. ```python # ✅ Good store_id store_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/stores'); await apiClient.post('/api/v1/products', data); // ❌ Bad await ApiClient.get('/api/v1/stores'); 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: Store Templates Extend store/base.html **Severity:** Error ```jinja ✅ {% extends "store/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
``` #### TPL-008: Use Valid Block Names **Severity:** Error Templates must use block names that exist in their base template. Using undefined blocks silently fails (content is not rendered). **Valid Admin Template Blocks:** - `title` - Page title - `extra_head` - Additional head content (CSS, meta tags) - `alpine_data` - Alpine.js component function name - `content` - Main page content - `extra_scripts` - Additional JavaScript ```jinja {# ✅ Good - Valid block names #} {% block extra_scripts %} {% endblock %} {# ❌ Bad - Invalid block name (silently ignored!) #} {% block page_scripts %} {% endblock %} ``` **Common mistakes detected:** - `page_scripts` → use `extra_scripts` - `scripts` → use `extra_scripts` - `js` → use `extra_scripts` - `head` → use `extra_head` --- ### 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 #}