# 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 ✅
Loading...
``` #### TPL-007: Implement Empty State **Severity:** Warning Show empty state when lists have no items. ```html ✅ ``` --- ### 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 #}
......
``` #### MAC-002: Import Macros at Template Top **Severity:** Warning All macro imports should be at the top of the template, after `{% extends %}`. ```jinja {# ✅ Good #} {% extends "admin/base.html" %} {% from 'shared/macros/pagination.html' import pagination %} {% from 'shared/macros/tables.html' import table_wrapper %} {% from 'shared/macros/alerts.html' import loading_state %} {% block content %} ... {% endblock %} ``` #### MAC-003: Use {% call %} for Wrapper Macros **Severity:** Error Wrapper macros that use `{{ caller() }}` must be invoked with `{% call %}`. ```jinja {# ✅ Good #} {% call table_wrapper() %} {{ table_header(['Col1', 'Col2']) }} {% endcall %} {% call tabs_nav() %} {{ tab_button('tab1', 'First') }} {% endcall %} {# ❌ Bad - Missing call block #} {{ table_wrapper() }} {# This won't render inner content! #} ``` #### MAC-004: Document New Macros **Severity:** Warning New macros must have JSDoc-style documentation comments. ```jinja {# ✅ Good #} {# Number Stepper ============== A number input with +/- buttons. Parameters: - model: Alpine.js x-model variable (required) - min: Minimum value (default: 1) - max: Maximum value (optional) Usage: {{ number_stepper(model='quantity', min=1, max=99) }} #} {% macro number_stepper(model, min=1, max=none) %} ... {% endmacro %} ``` #### MAC-005: Add Components to Reference Page **Severity:** Warning New shared components should be added to `/admin/components` page with live demos. --- ### Frontend Component Rules (app/templates/**/*.html) #### FE-008: Use number_stepper Macro for Quantity Inputs **Severity:** Warning Use the shared `number_stepper` macro instead of raw `` for consistent styling and dark mode support. ```jinja {# ✅ Good - Using number_stepper macro #} {% from 'shared/macros/inputs.html' import number_stepper %} {{ number_stepper(model='quantity', min=1, max=99) }} {# ❌ Bad - Raw number input #} ``` **Suppress with `noqa` for ID fields:** ```jinja {# noqa: FE-008 - User ID is typed directly, not incremented #} ``` --- ### Styling Rules (app/templates/**/*.html) #### CSS-001: Use Tailwind Utility Classes **Severity:** Warning Prefer Tailwind utility classes over custom CSS. #### CSS-002: Support Dark Mode **Severity:** Warning All color classes should include dark mode variants. ```html ✅ class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white" ❌ class="bg-white text-gray-900" ``` #### CSS-003: Shop Templates Use CSS Variables **Severity:** Error Shop templates must use CSS variables for vendor-specific theming. ```html ✅ ``` #### CSS-004: Mobile-First Responsive Design **Severity:** Warning Use mobile-first responsive classes. ```html ✅ class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4" ❌ class="grid grid-cols-4" ``` --- ## Security & Multi-Tenancy Rules ### Multi-Tenancy Rules #### MT-001: Scope All Queries to vendor_id **Severity:** Error In vendor/shop contexts, all database queries must filter by vendor_id. ```python # ✅ Good def get_products(self, db: Session, vendor_id: int): return db.query(Product).filter(Product.vendor_id == vendor_id).all() # ❌ Bad def get_products(self, db: Session): return db.query(Product).all() # Cross-tenant data leak! ``` #### MT-002: No Cross-Vendor Data Access **Severity:** Error Queries must never access data from other vendors. --- ### Authentication & Authorization Rules #### AUTH-001: Use JWT Tokens **Severity:** Error Authentication must use JWT tokens in Authorization: Bearer header. #### AUTH-002: Role-Based Access Control **Severity:** Error Use Depends(get_current_admin/vendor/customer) for role checks. ```python # ✅ Good @router.get("/admin/users") async def list_users(current_user: User = Depends(get_current_admin)): pass ``` #### AUTH-003: Never Store Plain Passwords **Severity:** Error Always hash passwords with bcrypt before storing. --- ## Middleware Rules #### MDW-001: Middleware Naming **Severity:** Warning Middleware files should be named with simple nouns (auth.py, not auth_middleware.py). ``` ✅ middleware/auth.py ✅ middleware/context.py ❌ middleware/auth_middleware.py ``` #### MDW-002: Vendor Context Injection **Severity:** Error Vendor context middleware must set `request.state.vendor_id` and `request.state.vendor`. --- ## Code Quality Rules #### QUAL-001: Format with Ruff **Severity:** Error All code must be formatted with Ruff before committing. ```bash make format ``` #### QUAL-002: Pass Ruff Linting **Severity:** Error All code must pass Ruff linting before committing. ```bash make lint ``` #### QUAL-003: Use Type Hints **Severity:** Warning Add type hints to function parameters and return types. ```python # ✅ Good def create_product(self, db: Session, data: ProductCreate) -> Product: pass # Better from typing import Optional def get_product(self, product_id: int) -> Optional[Product]: pass ``` --- ## Ignored Patterns The validator ignores these files/patterns: - Test files: `**/*_test.py`, `**/test_*.py` - Cache: `**/__pycache__/**` - Migrations: `**/alembic/versions/**` - Dependencies: `**/node_modules/**`, `**/.venv/**`, `**/venv/**` - Build artifacts: `**/build/**`, `**/dist/**` Special exceptions: - `app/core/exceptions.py` - Allowed to use HTTPException - `app/exceptions/handler.py` - Converts to HTTPException --- ## Quick Reference ### Pre-Commit Checklist Before committing code: - [ ] Run `make format` (Ruff formatting) - [ ] Run `make lint` (Ruff + mypy) - [ ] Run `python scripts/validate_architecture.py` - [ ] Fix all **Error** level violations - [ ] Review **Warning** level violations - [ ] Run relevant tests ### Common Violations and Fixes | Violation | Quick Fix | |-----------|-----------| | HTTPException in service | Create custom exception in `app/exceptions/` | | HTTPException in endpoint | Let domain exceptions bubble up to global handler | | Business logic in endpoint | Move to service layer | | console.log in JS | Use `window.LogConfig.createLogger()` | | Missing ...data() | Add spread operator in component return | | Bare except clause | Specify exception type | | Raw dict return | Create Pydantic response model | | Template not extending base | Add `{% extends %}` or `{# standalone #}` marker | --- ## Configuration All rules are defined in `.architecture-rules.yaml`. To modify rules: 1. Edit `.architecture-rules.yaml` 2. Update `scripts/validate_architecture.py` if implementing new checks 3. Run validator to test changes 4. Update this documentation --- ## Related Documentation - [Code Quality Guide](code-quality.md) - [Contributing Guide](contributing.md) - [Architecture Overview](../architecture/overview.md) - [Backend Development](../backend/overview.md) - [Frontend Development](../frontend/overview.md) --- ## Summary Statistics | Category | Rules | Errors | Warnings | |----------|-------|--------|----------| | Backend | 20 | 15 | 5 | | Frontend JS | 7 | 6 | 1 | | Frontend Templates | 7 | 3 | 4 | | Frontend Macros | 5 | 2 | 3 | | Frontend Components | 1 | 0 | 1 | | Frontend Styling | 4 | 1 | 3 | | Naming | 5 | 3 | 2 | | Security | 5 | 5 | 0 | | Quality | 3 | 2 | 1 | | **Total** | **57** | **37** | **20** | --- **Last Updated:** 2025-12-07 **Version:** 2.3