Files
orion/docs/development/architecture-rules.md
Samir Boulahtit 95a8ffc645 docs: add architecture rules and docs for e-commerce components
Architecture rules added:
- FE-008: Use number_stepper macro for quantity inputs
- FE-009: Use product_card macro for product displays
- FE-010: Use product_grid macro for product listings
- FE-011: Use add_to_cart macros for cart interactions
- FE-012: Use mini_cart macro for cart dropdown

Documentation:
- Update ui-components-quick-reference.md with e-commerce section
- Add component-standards.md for standardization guidelines
- Add ecommerce-components-proposal.md with full 20-component roadmap
- Update validate_architecture.py with FE-008 detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-07 17:04:28 +01:00

24 KiB

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

# 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

# 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.

# ✅ 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.

# ✅ 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.

# ✅ 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.

# ✅ 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:

# ✅ 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.

# ✅ 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.

# ✅ 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.

# ✅ 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

# ❌ 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.

# ✅ 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.

# ✅ 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

# ❌ 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.

# ✅ 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.

# ✅ Good
vendor_id
vendor_service

# ❌ Bad
shop_id
shop_service

NAM-005: Use 'inventory' not 'stock'

Severity: Warning

# ✅ 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().

// ✅ 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

// ✅ 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.

// ✅ 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.

// ✅ Good
return {
    ...data(),
    currentPage: 'dashboard',  // Required!
    // ...
};

JS-005: Initialization Guards

Severity: Error

Init methods should prevent duplicate initialization.

// ✅ 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.

// ✅ 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.

// ✅ 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.).

{# ✅ 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:

{# standalone - Minimal monitoring page without admin chrome #}
<!DOCTYPE html>
<html>
...

Recognized markers:

  • {# standalone #} - Jinja comment style
  • {# noqa: TPL-001 #} - Standard noqa style
  • <!-- standalone --> - HTML comment style

TPL-002: Vendor Templates Extend vendor/base.html

Severity: Error

{% extends "vendor/base.html" %}

TPL-003: Shop Templates Extend shop/base.html

Severity: Error

{% 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.

<p x-text="item.name"></p><p>{{ item.name }}</p>

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.

<span x-html="$icon('home', 'w-5 h-5')"></span><div x-html="userComment"></div>  <!-- XSS vulnerability! -->

TPL-006: Implement Loading State

Severity: Warning

All templates that load data should show loading state.

<div x-show="loading">Loading...</div>

TPL-007: Implement Empty State

Severity: Warning

Show empty state when lists have no items.

<template x-if="items.length === 0">
    <p>No items found</p>
</template>

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/.

{# ✅ Good - Using shared macro #}
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
{% call table_wrapper() %}
    {{ table_header(['Name', 'Email', 'Status']) }}
    <tbody>...</tbody>
{% endcall %}

{# ❌ Bad - Copy-pasting table HTML #}
<div class="w-full overflow-hidden rounded-lg shadow-xs">
    <div class="w-full overflow-x-auto">
        <table class="w-full whitespace-no-wrap">
            <thead>...</thead>
            <tbody>...</tbody>
        </table>
    </div>
</div>

MAC-002: Import Macros at Template Top

Severity: Warning

All macro imports should be at the top of the template, after {% extends %}.

{# ✅ 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 %}.

{# ✅ 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.

{# ✅ 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 <input type="number"> for consistent styling and dark mode support.

{# ✅ 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 #}
<input type="number" x-model="quantity" min="1" max="99" />

Suppress with noqa for ID fields:

{# noqa: FE-008 - User ID is typed directly, not incremented #}
<input type="number" x-model="userId" placeholder="User ID" />

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.

✅ 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.

<button style="background-color: var(--color-primary)">Buy Now</button><button class="bg-blue-600">Buy Now</button>

CSS-004: Mobile-First Responsive Design

Severity: Warning

Use mobile-first responsive classes.

✅ 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.

# ✅ 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.

# ✅ 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.

make format

QUAL-002: Pass Ruff Linting

Severity: Error

All code must pass Ruff linting before committing.

make lint

QUAL-003: Use Type Hints

Severity: Warning

Add type hints to function parameters and return types.

# ✅ 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


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