diff --git a/.architecture-rules.yaml b/.architecture-rules.yaml
new file mode 100644
index 00000000..73c62cb9
--- /dev/null
+++ b/.architecture-rules.yaml
@@ -0,0 +1,407 @@
+# Architecture Rules Configuration
+# 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"
+project: "letzshop-product-import"
+
+# ============================================================================
+# CORE ARCHITECTURAL PRINCIPLES
+# ============================================================================
+
+principles:
+ - name: "Separation of Concerns"
+ description: "API endpoints should only handle HTTP concerns. Business logic belongs in services."
+
+ - name: "Layered Architecture"
+ description: "Routes → Services → Models. Each layer has specific responsibilities."
+
+ - name: "Type Safety"
+ description: "Use Pydantic models for request/response validation. Use SQLAlchemy models for database."
+
+ - name: "Proper Exception Handling"
+ description: "Services throw domain exceptions. Routes catch and convert to HTTPException."
+
+# ============================================================================
+# API ENDPOINT RULES (app/api/v1/**/*.py)
+# ============================================================================
+
+api_endpoint_rules:
+
+ - id: "API-001"
+ name: "Endpoint must use Pydantic models for request/response"
+ severity: "error"
+ description: |
+ All API endpoints must use Pydantic models (BaseModel) for request bodies
+ 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
+ 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!
+
+ - id: "API-002"
+ name: "Endpoint must NOT contain business logic"
+ severity: "error"
+ description: |
+ API endpoints should only handle HTTP concerns (validation, auth, response formatting).
+ All business logic must be delegated to service layer.
+ pattern:
+ file_pattern: "app/api/v1/**/*.py"
+ anti_patterns:
+ - "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"
+ severity: "error"
+ description: |
+ API endpoints must catch domain exceptions from services and convert them
+ 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
+
+ - id: "API-004"
+ name: "Endpoint must have proper authentication/authorization"
+ severity: "warning"
+ description: |
+ Protected endpoints must use Depends() for authentication.
+ Use get_current_user, get_current_admin, etc.
+ pattern:
+ 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
+
+# ============================================================================
+# SERVICE LAYER RULES (app/services/**/*.py)
+# ============================================================================
+
+service_layer_rules:
+
+ - id: "SVC-001"
+ name: "Service must NOT raise HTTPException"
+ severity: "error"
+ description: |
+ Services are business logic layer - they should NOT know about HTTP.
+ Raise domain-specific exceptions instead (ValueError, custom exceptions).
+ pattern:
+ file_pattern: "app/services/**/*.py"
+ 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"
+ severity: "error"
+ description: |
+ Services should raise meaningful domain exceptions, not generic Exception.
+ 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!
+
+ - id: "SVC-003"
+ name: "Service methods must accept db session as parameter"
+ severity: "error"
+ description: |
+ Service methods should receive database session as a parameter for testability
+ and transaction control. Never create session inside service.
+ pattern:
+ file_pattern: "app/services/**/*.py"
+ required_in_method_signature:
+ - "db: Session"
+ 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"
+ severity: "warning"
+ description: |
+ Service methods should accept Pydantic models for complex inputs
+ to ensure type safety and validation.
+ pattern:
+ file_pattern: "app/services/**/*.py"
+ encouraged_patterns:
+ - "def .+\\(.*: BaseModel"
+ - "def .+\\(.*: .*Create"
+ - "def .+\\(.*: .*Update"
+
+# ============================================================================
+# MODEL RULES (app/models/**/*.py)
+# ============================================================================
+
+model_rules:
+
+ - id: "MDL-001"
+ name: "Database models must use SQLAlchemy Base"
+ severity: "error"
+ description: |
+ All database models must inherit from SQLAlchemy Base and use proper
+ column definitions with types and constraints.
+ pattern:
+ file_pattern: "app/models/**/*.py"
+ required_patterns:
+ - "class.*\\(Base\\):"
+ - "from.*sqlalchemy.*import.*Column"
+
+ - id: "MDL-002"
+ name: "Use Pydantic models separately from SQLAlchemy models"
+ severity: "error"
+ description: |
+ Never mix SQLAlchemy and Pydantic in the same model.
+ SQLAlchemy = database schema, Pydantic = API validation/serialization.
+ pattern:
+ file_pattern: "app/models/**/*.py"
+ anti_patterns:
+ - "class.*\\(Base, BaseModel\\):" # Multiple inheritance - BAD!
+
+# ============================================================================
+# EXCEPTION HANDLING RULES
+# ============================================================================
+
+exception_rules:
+
+ - id: "EXC-001"
+ name: "Define custom exceptions in exceptions module"
+ severity: "warning"
+ description: |
+ Create domain-specific exceptions in app/exceptions/ for better
+ error handling and clarity.
+ 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"
+ description: |
+ Always specify exception types. Bare except catches everything including
+ KeyboardInterrupt and SystemExit.
+ pattern:
+ file_pattern: "**/*.py"
+ 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
+
+# ============================================================================
+# JAVASCRIPT ARCHITECTURE RULES
+# ============================================================================
+
+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"
+ pattern:
+ file_pattern: "static/admin/js/**/*.js"
+ anti_patterns:
+ - "console\\.log"
+ - "console\\.error"
+ - "console\\.warn"
+ exceptions:
+ - "// eslint-disable"
+ - "console.log('✅" # Bootstrap messages allowed
+
+ - id: "JS-003"
+ name: "Alpine components must spread ...data()"
+ severity: "error"
+ description: "All Alpine.js components must inherit base layout data"
+ pattern:
+ file_pattern: "static/admin/js/**/*.js"
+ required_in_alpine_components:
+ - "\\.\\.\\.data\\(\\)"
+
+# ============================================================================
+# TEMPLATE RULES
+# ============================================================================
+
+template_rules:
+
+ - id: "TPL-001"
+ name: "Admin templates must extend base.html"
+ severity: "error"
+ description: "All admin templates must extend the base template for consistency"
+ pattern:
+ file_pattern: "app/templates/admin/**/*.html"
+ required_patterns:
+ - "{% extends ['\"]admin/base\\.html['\"] %}"
+ exceptions:
+ - "base.html"
+ - "partials/"
+
+# ============================================================================
+# VALIDATION SEVERITY LEVELS
+# ============================================================================
+
+severity_levels:
+ error:
+ description: "Critical architectural violation - must be fixed"
+ exit_code: 1
+
+ warning:
+ description: "Pattern deviation - should be fixed"
+ exit_code: 0 # Don't fail build, but report
+
+ info:
+ description: "Suggestion for improvement"
+ exit_code: 0
+
+# ============================================================================
+# IGNORED PATTERNS (False Positives)
+# ============================================================================
+
+ignore:
+ files:
+ - "**/*_test.py"
+ - "**/test_*.py"
+ - "**/__pycache__/**"
+ - "**/migrations/**"
+ - "**/node_modules/**"
+
+ patterns:
+ # Allow HTTPException in specific files
+ - file: "app/core/exceptions.py"
+ pattern: "HTTPException"
+ reason: "Exception handling utilities"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..eadb04d0
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,43 @@
+# Pre-commit hooks configuration
+# Install: pip install pre-commit
+# Setup: pre-commit install
+# Run manually: pre-commit run --all-files
+
+repos:
+ # Architecture validation
+ - repo: local
+ hooks:
+ - id: validate-architecture
+ name: Validate Architecture Patterns
+ entry: python scripts/validate_architecture.py
+ language: python
+ pass_filenames: false
+ always_run: true
+ additional_dependencies: [pyyaml]
+ verbose: true
+
+ # Python code quality
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-added-large-files
+ args: ['--maxkb=1000']
+ - id: check-json
+ - id: check-merge-conflict
+ - id: debug-statements
+
+ # Python formatting (optional - uncomment if you want)
+ # - repo: https://github.com/psf/black
+ # rev: 23.12.1
+ # hooks:
+ # - id: black
+ # language_version: python3
+
+ # Python import sorting (optional)
+ # - repo: https://github.com/pycqa/isort
+ # rev: 5.13.2
+ # hooks:
+ # - id: isort
diff --git a/app/templates/admin/content-pages.html b/app/templates/admin/content-pages.html
index 9ab62559..500611de 100644
--- a/app/templates/admin/content-pages.html
+++ b/app/templates/admin/content-pages.html
@@ -157,14 +157,14 @@
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
title="Edit"
>
-
+
diff --git a/docs/architecture/architecture-patterns.md b/docs/architecture/architecture-patterns.md
new file mode 100644
index 00000000..93640be9
--- /dev/null
+++ b/docs/architecture/architecture-patterns.md
@@ -0,0 +1,611 @@
+# Architecture Patterns & Design Decisions
+
+This document describes the architectural patterns and design decisions that must be followed throughout the codebase.
+
+> **Note:** These patterns are enforced automatically by `scripts/validate_architecture.py`. Run the validator before committing code.
+
+---
+
+## Table of Contents
+
+1. [Core Principles](#core-principles)
+2. [Layered Architecture](#layered-architecture)
+3. [API Endpoint Patterns](#api-endpoint-patterns)
+4. [Service Layer Patterns](#service-layer-patterns)
+5. [Model Patterns](#model-patterns)
+6. [Exception Handling](#exception-handling)
+7. [JavaScript Patterns](#javascript-patterns)
+8. [Validation](#validation)
+
+---
+
+## Core Principles
+
+### 1. Separation of Concerns
+
+**Each layer has specific responsibilities:**
+
+- **Routes/Endpoints**: HTTP handling, validation, authentication, response formatting
+- **Services**: Business logic, data processing, orchestration
+- **Models**: Data structure and persistence
+
+**❌ Bad Example - Business logic in endpoint:**
+
+```python
+@router.post("/vendors")
+async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
+ # ❌ BAD: Business logic in endpoint
+ if db.query(Vendor).filter(Vendor.subdomain == vendor.subdomain).first():
+ raise HTTPException(status_code=409, detail="Vendor exists")
+
+ db_vendor = Vendor(**vendor.dict())
+ db.add(db_vendor)
+ db.commit()
+ db.refresh(db_vendor)
+ return db_vendor
+```
+
+**✅ Good Example - Delegated to service:**
+
+```python
+@router.post("/vendors", response_model=VendorResponse)
+async def create_vendor(
+ vendor: VendorCreate,
+ current_user: User = Depends(get_current_admin),
+ db: Session = Depends(get_db)
+):
+ try:
+ # ✅ GOOD: Delegate to service
+ 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"Failed to create vendor: {e}")
+ raise HTTPException(status_code=500, detail="Internal server error")
+```
+
+### 2. Type Safety
+
+Use Pydantic for API validation, SQLAlchemy for database models.
+
+### 3. Proper Exception Handling
+
+Services throw domain exceptions, routes convert to HTTP responses.
+
+---
+
+## Layered Architecture
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ API Layer │
+│ (app/api/v1/**/*.py, app/routes/**/*.py) │
+│ │
+│ Responsibilities: │
+│ - HTTP request/response handling │
+│ - Authentication/Authorization │
+│ - Input validation (Pydantic models) │
+│ - Exception handling (domain → HTTP) │
+│ │
+│ ❌ Should NOT: │
+│ - Contain business logic │
+│ - Directly access database (except via services) │
+│ - Raise domain exceptions │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ Service Layer │
+│ (app/services/**/*.py) │
+│ │
+│ Responsibilities: │
+│ - Business logic │
+│ - Data validation (business rules) │
+│ - Database operations │
+│ - Orchestration of multiple operations │
+│ │
+│ ❌ Should NOT: │
+│ - Know about HTTP (no HTTPException) │
+│ - Create database sessions (accept as parameter) │
+│ - Handle HTTP-specific concerns │
+└─────────────────────────────────────────────────────────┘
+ ↓
+┌─────────────────────────────────────────────────────────┐
+│ Data Layer │
+│ (app/models/**/*.py) │
+│ │
+│ Responsibilities: │
+│ - Database schema (SQLAlchemy models) │
+│ - API schemas (Pydantic models) │
+│ - Data structure definitions │
+│ │
+│ ❌ Should NOT: │
+│ - Mix SQLAlchemy and Pydantic in same class │
+│ - Contain business logic │
+└─────────────────────────────────────────────────────────┘
+```
+
+---
+
+## API Endpoint Patterns
+
+### Rule API-001: Use Pydantic Models
+
+**All endpoints MUST use Pydantic models for request/response.**
+
+```python
+# ✅ GOOD: Pydantic models for type safety
+class VendorCreate(BaseModel):
+ name: str = Field(..., max_length=200)
+ subdomain: str = Field(..., max_length=100)
+ is_active: bool = True
+
+class VendorResponse(BaseModel):
+ id: int
+ name: str
+ subdomain: str
+ created_at: datetime
+
+ class Config:
+ from_attributes = True # For SQLAlchemy compatibility
+
+@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
+```
+
+```python
+# ❌ BAD: Raw dict, no validation
+@router.post("/vendors")
+async def create_vendor(data: dict):
+ return {"name": data["name"]} # No type safety!
+```
+
+### Rule API-002: No Business Logic in Endpoints
+
+**Endpoints should only handle HTTP concerns.**
+
+```python
+# ✅ GOOD: Delegate to service
+@router.post("/vendors")
+async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
+ result = vendor_service.create_vendor(db, vendor)
+ return result
+```
+
+```python
+# ❌ BAD: Business logic in endpoint
+@router.post("/vendors")
+async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
+ # ❌ Database operations belong in service!
+ db_vendor = Vendor(**vendor.dict())
+ db.add(db_vendor)
+ db.commit()
+ return db_vendor
+```
+
+### Rule API-003: Proper Exception Handling
+
+**Catch service exceptions and convert to HTTPException.**
+
+```python
+# ✅ GOOD: Proper exception handling
+@router.post("/vendors", response_model=VendorResponse)
+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 ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ logger.error(f"Unexpected error creating vendor: {e}")
+ raise HTTPException(status_code=500, detail="Internal server error")
+```
+
+### Rule API-004: Authentication
+
+**Protected endpoints must use dependency injection for auth.**
+
+```python
+# ✅ GOOD: Use Depends for auth
+@router.post("/vendors")
+async def create_vendor(
+ vendor: VendorCreate,
+ current_user: User = Depends(get_current_admin), # ✅ Auth required
+ db: Session = Depends(get_db)
+):
+ result = vendor_service.create_vendor(db, vendor)
+ return result
+```
+
+---
+
+## Service Layer Patterns
+
+### Rule SVC-001: No HTTPException in Services
+
+**Services should NOT know about HTTP. Raise domain exceptions instead.**
+
+```python
+# ✅ GOOD: Domain exception
+class VendorAlreadyExistsError(Exception):
+ """Raised when vendor with same subdomain already exists"""
+ pass
+
+class VendorService:
+ def create_vendor(self, db: Session, vendor_data: VendorCreate):
+ if self._vendor_exists(db, vendor_data.subdomain):
+ raise VendorAlreadyExistsError(
+ f"Vendor with subdomain '{vendor_data.subdomain}' already exists"
+ )
+
+ # Business logic...
+ vendor = Vendor(**vendor_data.dict())
+ db.add(vendor)
+ db.commit()
+ return vendor
+```
+
+```python
+# ❌ BAD: HTTPException in service
+class VendorService:
+ def create_vendor(self, db: Session, vendor_data: VendorCreate):
+ if self._vendor_exists(db, vendor_data.subdomain):
+ # ❌ Service shouldn't know about HTTP!
+ raise HTTPException(status_code=409, detail="Vendor exists")
+```
+
+### Rule SVC-002: Create Custom Exception Classes
+
+**Don't use generic `Exception`. Create specific domain exceptions.**
+
+```python
+# ✅ GOOD: Specific exceptions
+class VendorError(Exception):
+ """Base exception for vendor-related errors"""
+ pass
+
+class VendorNotFoundError(VendorError):
+ """Raised when vendor is not found"""
+ pass
+
+class VendorAlreadyExistsError(VendorError):
+ """Raised when vendor already exists"""
+ pass
+
+class VendorService:
+ def get_vendor(self, db: Session, vendor_code: str):
+ vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code).first()
+ if not vendor:
+ raise VendorNotFoundError(f"Vendor '{vendor_code}' not found")
+ return vendor
+```
+
+```python
+# ❌ BAD: Generic Exception
+class VendorService:
+ def get_vendor(self, db: Session, vendor_code: str):
+ vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code).first()
+ if not vendor:
+ raise Exception("Vendor not found") # ❌ Too generic!
+ return vendor
+```
+
+### Rule SVC-003: Database Session as Parameter
+
+**Services should receive database session as parameter, not create it internally.**
+
+```python
+# ✅ GOOD: db session as parameter
+class VendorService:
+ def create_vendor(self, db: Session, vendor_data: VendorCreate):
+ vendor = Vendor(**vendor_data.dict())
+ db.add(vendor)
+ db.commit()
+ db.refresh(vendor)
+ return vendor
+
+ def _vendor_exists(self, db: Session, subdomain: str) -> bool:
+ return db.query(Vendor).filter(Vendor.subdomain == subdomain).first() is not None
+```
+
+```python
+# ❌ BAD: Creating session internally
+class VendorService:
+ def create_vendor(self, vendor_data: VendorCreate):
+ # ❌ Don't create session here - makes testing hard
+ db = SessionLocal()
+ vendor = Vendor(**vendor_data.dict())
+ db.add(vendor)
+ db.commit()
+ return vendor
+```
+
+**Benefits:**
+
+- Testability (can inject mock session)
+- Transaction control (caller controls commit/rollback)
+- Resource management (caller handles session lifecycle)
+
+### Rule SVC-004: Use Pydantic for Input Validation
+
+**Service methods should accept Pydantic models for complex inputs.**
+
+```python
+# ✅ GOOD: Pydantic model ensures validation
+class VendorService:
+ def create_vendor(self, db: Session, vendor_data: VendorCreate):
+ # vendor_data is already validated by Pydantic
+ vendor = Vendor(**vendor_data.dict())
+ db.add(vendor)
+ db.commit()
+ return vendor
+```
+
+---
+
+## Model Patterns
+
+### Rule MDL-001: SQLAlchemy for Database Models
+
+```python
+# ✅ GOOD: SQLAlchemy model
+from sqlalchemy import Column, Integer, String, Boolean
+from app.database import Base
+
+class Vendor(Base):
+ __tablename__ = "vendors"
+
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String(200), nullable=False)
+ subdomain = Column(String(100), unique=True, nullable=False)
+ is_active = Column(Boolean, default=True)
+```
+
+### Rule MDL-002: Separate Pydantic from SQLAlchemy
+
+**NEVER mix SQLAlchemy and Pydantic in the same class.**
+
+```python
+# ❌ BAD: Mixing SQLAlchemy and Pydantic
+class Vendor(Base, BaseModel): # ❌ Don't do this!
+ __tablename__ = "vendors"
+ name: str = Column(String(200))
+```
+
+```python
+# ✅ GOOD: Separate models
+# Database model (app/models/vendor.py)
+class Vendor(Base):
+ __tablename__ = "vendors"
+ id = Column(Integer, primary_key=True)
+ name = Column(String(200), nullable=False)
+
+# API model (app/api/v1/admin/vendors.py)
+class VendorCreate(BaseModel):
+ name: str = Field(..., max_length=200)
+
+class VendorResponse(BaseModel):
+ id: int
+ name: str
+
+ class Config:
+ from_attributes = True
+```
+
+---
+
+## Exception Handling
+
+### Rule EXC-001: Domain-Specific Exceptions
+
+**Create exception hierarchy in `app/exceptions/`**
+
+```python
+# app/exceptions/vendor_exceptions.py
+
+class VendorError(Exception):
+ """Base exception for vendor domain"""
+ pass
+
+class VendorNotFoundError(VendorError):
+ """Vendor does not exist"""
+ pass
+
+class VendorAlreadyExistsError(VendorError):
+ """Vendor already exists"""
+ pass
+
+class VendorValidationError(VendorError):
+ """Vendor data validation failed"""
+ pass
+```
+
+### Rule EXC-002: Never Use Bare Except
+
+```python
+# ❌ BAD: Bare except
+try:
+ result = do_something()
+except: # ❌ Catches EVERYTHING including KeyboardInterrupt!
+ pass
+```
+
+```python
+# ✅ GOOD: Specific exceptions
+try:
+ result = do_something()
+except ValueError as e:
+ logger.error(f"Validation error: {e}")
+except DatabaseError as e:
+ logger.error(f"Database error: {e}")
+except Exception as e:
+ logger.error(f"Unexpected error: {e}")
+ raise
+```
+
+---
+
+## JavaScript Patterns
+
+### Rule JS-001: Use apiClient Directly
+
+```javascript
+// ✅ GOOD
+const vendors = await apiClient.get('/api/v1/vendors');
+```
+
+```javascript
+// ❌ BAD
+const vendors = await window.apiClient.get('/api/v1/vendors');
+```
+
+### Rule JS-002: Use Centralized Logger
+
+```javascript
+// ✅ GOOD: Centralized logger
+const vendorLog = window.LogConfig.createLogger('vendors');
+vendorLog.info('Loading vendors...');
+vendorLog.error('Failed to load vendors:', error);
+```
+
+```javascript
+// ❌ BAD: console
+console.log('Loading vendors...'); // ❌ Use logger instead
+```
+
+### Rule JS-003: Alpine Components Pattern
+
+```javascript
+// ✅ GOOD: Proper Alpine component
+function vendorsManager() {
+ return {
+ // ✅ Inherit base layout functionality
+ ...data(),
+
+ // ✅ Set page identifier for sidebar
+ currentPage: 'vendors',
+
+ // Component state
+ vendors: [],
+ loading: false,
+
+ // ✅ Init with guard
+ async init() {
+ if (window._vendorsInitialized) {
+ return;
+ }
+ window._vendorsInitialized = true;
+
+ await this.loadVendors();
+ }
+ };
+}
+```
+
+---
+
+## Validation
+
+### Running the Validator
+
+```bash
+# Validate entire codebase
+python scripts/validate_architecture.py
+
+# Validate specific directory
+python scripts/validate_architecture.py app/api/
+
+# Verbose output with context
+python scripts/validate_architecture.py --verbose
+
+# Errors only (suppress warnings)
+python scripts/validate_architecture.py --errors-only
+```
+
+### Pre-commit Hook
+
+Install pre-commit to validate automatically before commits:
+
+```bash
+# Install pre-commit
+pip install pre-commit
+
+# Setup hooks
+pre-commit install
+
+# Run manually
+pre-commit run --all-files
+```
+
+### CI/CD Integration
+
+Add to your CI pipeline:
+
+```yaml
+# .github/workflows/ci.yml
+- name: Validate Architecture
+ run: |
+ python scripts/validate_architecture.py
+```
+
+---
+
+## Quick Reference
+
+| Layer | Responsibility | Can Use | Cannot Use |
+|-------|---------------|---------|------------|
+| **API Endpoints** | HTTP handling, auth, validation | Pydantic, HTTPException, Depends | Direct DB access, business logic |
+| **Services** | Business logic, orchestration | DB session, domain exceptions | HTTPException, HTTP concepts |
+| **Models** | Data structure | SQLAlchemy OR Pydantic | Mixing both in same class |
+
+---
+
+## Common Violations and Fixes
+
+### Violation: Business logic in endpoint
+
+```python
+# Before
+@router.post("/vendors")
+async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
+ db_vendor = Vendor(**vendor.dict())
+ db.add(db_vendor)
+ db.commit()
+ return db_vendor
+```
+
+```python
+# After ✅
+@router.post("/vendors")
+async def create_vendor(vendor: VendorCreate, db: Session = Depends(get_db)):
+ try:
+ return vendor_service.create_vendor(db, vendor)
+ except VendorAlreadyExistsError as e:
+ raise HTTPException(status_code=409, detail=str(e))
+```
+
+### Violation: HTTPException in service
+
+```python
+# Before
+class VendorService:
+ def create_vendor(self, db: Session, vendor_data):
+ if exists:
+ raise HTTPException(status_code=409, detail="Exists")
+```
+
+```python
+# After ✅
+class VendorService:
+ def create_vendor(self, db: Session, vendor_data):
+ if exists:
+ raise VendorAlreadyExistsError("Vendor already exists")
+```
+
+---
+
+**Remember:** These patterns are enforced automatically. Run `python scripts/validate_architecture.py` before committing!
diff --git a/mkdocs.yml b/mkdocs.yml
index b0e3ace2..beb6abc2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -29,6 +29,7 @@ nav:
# ============================================
- Architecture:
- Overview: architecture/overview.md
+ - Architecture Patterns: architecture/architecture-patterns.md
- Multi-Tenant System: architecture/multi-tenant.md
- Middleware Stack: architecture/middleware.md
- Request Flow: architecture/request-flow.md
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 00000000..2a6d12ac
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,227 @@
+# Architecture Validation Scripts
+
+This directory contains scripts for validating and enforcing architectural patterns across the codebase.
+
+## Architecture Validator
+
+### Overview
+
+`validate_architecture.py` is an automated tool that checks the codebase against architectural rules defined in `.architecture-rules.yaml`.
+
+### Key Features
+
+- **API Endpoint Validation**: Ensures proper separation of concerns, Pydantic usage, exception handling
+- **Service Layer Validation**: Enforces domain exceptions, database session patterns
+- **Model Validation**: Checks proper separation of SQLAlchemy and Pydantic models
+- **Exception Handling**: Validates proper exception patterns
+- **JavaScript Patterns**: Enforces coding standards for frontend code
+- **Template Validation**: Ensures templates follow required patterns
+
+### Usage
+
+```bash
+# Validate entire codebase
+python scripts/validate_architecture.py
+
+# Validate specific directory
+python scripts/validate_architecture.py app/api/
+
+# Verbose output with code context
+python scripts/validate_architecture.py --verbose
+
+# Show only errors (suppress warnings)
+python scripts/validate_architecture.py --errors-only
+
+# Use custom config file
+python scripts/validate_architecture.py --config custom-rules.yaml
+```
+
+### Exit Codes
+
+- `0`: Validation passed (warnings allowed)
+- `1`: Validation failed (errors found)
+
+### Integration with Pre-commit
+
+Install pre-commit hooks to run validation automatically:
+
+```bash
+# Install pre-commit
+pip install pre-commit
+
+# Setup hooks
+pre-commit install
+
+# Run manually on all files
+pre-commit run --all-files
+
+# Run on staged files only
+pre-commit run
+```
+
+### Configuration
+
+The validation rules are defined in `.architecture-rules.yaml` at the project root. This file contains:
+
+- **Core Principles**: Separation of concerns, type safety, exception handling
+- **API Endpoint Rules**: (API-001 through API-004)
+- **Service Layer Rules**: (SVC-001 through SVC-004)
+- **Model Rules**: (MDL-001 through MDL-002)
+- **Exception Rules**: (EXC-001 through EXC-002)
+- **JavaScript Rules**: (JS-001 through JS-003)
+- **Template Rules**: (TPL-001)
+
+### Rule Categories
+
+#### API Endpoint Rules
+
+- **API-001**: Use Pydantic models for request/response
+- **API-002**: No business logic in endpoints
+- **API-003**: Proper exception handling (catch and convert to HTTPException)
+- **API-004**: Authentication on protected endpoints
+
+#### Service Layer Rules
+
+- **SVC-001**: No HTTPException in services (use domain exceptions)
+- **SVC-002**: Create custom exception classes (avoid generic Exception)
+- **SVC-003**: Database session as parameter (not created internally)
+- **SVC-004**: Use Pydantic for input validation
+
+#### Model Rules
+
+- **MDL-001**: Use SQLAlchemy Base for database models
+- **MDL-002**: Separate Pydantic from SQLAlchemy models
+
+#### Exception Rules
+
+- **EXC-001**: Define custom exceptions in exceptions module
+- **EXC-002**: Never use bare except
+
+#### JavaScript Rules
+
+- **JS-001**: Use apiClient directly (not window.apiClient)
+- **JS-002**: Use centralized logger (not console)
+- **JS-003**: Alpine components must spread ...data()
+
+### Example Output
+
+```
+🔍 Starting architecture validation...
+
+📡 Validating API endpoints...
+🔧 Validating service layer...
+📦 Validating models...
+⚠️ Validating exception handling...
+🟨 Validating JavaScript...
+📄 Validating templates...
+
+================================================================================
+📊 ARCHITECTURE VALIDATION REPORT
+================================================================================
+
+Files checked: 145
+Total violations: 3
+
+❌ ERRORS (2):
+--------------------------------------------------------------------------------
+
+ [API-002] Endpoint must NOT contain business logic
+ File: app/api/v1/admin/vendors.py:45
+ Issue: Database operations should be in service layer
+ 💡 Suggestion: Move database operations to service layer
+
+ [SVC-001] Service must NOT raise HTTPException
+ File: app/services/vendor_service.py:78
+ Issue: Service raises HTTPException - use domain exceptions instead
+ 💡 Suggestion: Create custom exception class (e.g., VendorNotFoundError) and raise that
+
+⚠️ WARNINGS (1):
+--------------------------------------------------------------------------------
+
+ [JS-001] Use apiClient directly
+ File: static/admin/js/vendors.js:23
+ Issue: Use apiClient directly instead of window.apiClient
+ 💡 Suggestion: Replace window.apiClient with apiClient
+
+================================================================================
+❌ VALIDATION FAILED - Fix errors before committing
+================================================================================
+```
+
+### CI/CD Integration
+
+Add to your CI pipeline (GitHub Actions example):
+
+```yaml
+# .github/workflows/ci.yml
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ validate-architecture:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+
+ - name: Install dependencies
+ run: |
+ pip install pyyaml
+
+ - name: Validate Architecture
+ run: |
+ python scripts/validate_architecture.py
+```
+
+### Documentation
+
+For detailed architectural patterns and examples, see:
+
+- [Architecture Patterns Documentation](../docs/architecture/architecture-patterns.md)
+- [Architecture Rules Config](../.architecture-rules.yaml)
+
+### Extending the Validator
+
+To add new rules:
+
+1. Add rule definition to `.architecture-rules.yaml`
+2. Implement validation logic in `validate_architecture.py`
+3. Add examples to `docs/architecture/architecture-patterns.md`
+4. Test with `python scripts/validate_architecture.py --verbose`
+
+### Troubleshooting
+
+**Issue**: Validator reports false positives
+
+**Solution**: Add exception patterns to `.architecture-rules.yaml` under `ignore` section
+
+**Issue**: Validator doesn't catch a specific violation
+
+**Solution**: The rule might not be implemented yet. Check `.architecture-rules.yaml` and `validate_architecture.py` to add the rule.
+
+**Issue**: Pre-commit hook fails but manual run passes
+
+**Solution**: Ensure pre-commit is using the same Python environment:
+```bash
+pre-commit clean
+pre-commit install
+```
+
+### Contributing
+
+When adding new architectural patterns:
+
+1. Document the pattern in `docs/architecture/architecture-patterns.md`
+2. Add validation rule to `.architecture-rules.yaml`
+3. Implement validation in `validate_architecture.py`
+4. Test thoroughly with existing codebase
+5. Update this README with the new rule
+
+---
+
+**Questions?** See the [Architecture Patterns Documentation](../docs/architecture/architecture-patterns.md)
diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py
new file mode 100755
index 00000000..38f2081f
--- /dev/null
+++ b/scripts/validate_architecture.py
@@ -0,0 +1,652 @@
+#!/usr/bin/env python3
+"""
+Architecture Validator
+======================
+Validates code against architectural rules defined in .architecture-rules.yaml
+
+This script checks that the codebase follows key architectural decisions:
+- Separation of concerns (routes vs services)
+- Proper exception handling (domain exceptions vs HTTPException)
+- Correct use of Pydantic vs SQLAlchemy models
+- Service layer patterns
+- API endpoint patterns
+
+Usage:
+ python scripts/validate_architecture.py # Check all files
+ python scripts/validate_architecture.py --fix # Auto-fix where possible
+ python scripts/validate_architecture.py --verbose # Detailed output
+ python scripts/validate_architecture.py app/api/ # Check specific directory
+"""
+
+import argparse
+import ast
+import re
+import sys
+from pathlib import Path
+from typing import List, Dict, Any, Tuple
+from dataclasses import dataclass, field
+from enum import Enum
+import yaml
+
+
+class Severity(Enum):
+ """Validation severity levels"""
+ ERROR = "error"
+ WARNING = "warning"
+ INFO = "info"
+
+
+@dataclass
+class Violation:
+ """Represents an architectural rule violation"""
+ rule_id: str
+ rule_name: str
+ severity: Severity
+ file_path: Path
+ line_number: int
+ message: str
+ context: str = ""
+ suggestion: str = ""
+
+
+@dataclass
+class ValidationResult:
+ """Results of architecture validation"""
+ violations: List[Violation] = field(default_factory=list)
+ files_checked: int = 0
+ rules_applied: int = 0
+
+ def has_errors(self) -> bool:
+ """Check if there are any error-level violations"""
+ return any(v.severity == Severity.ERROR for v in self.violations)
+
+ def has_warnings(self) -> bool:
+ """Check if there are any warning-level violations"""
+ return any(v.severity == Severity.WARNING for v in self.violations)
+
+
+class ArchitectureValidator:
+ """Main validator class"""
+
+ def __init__(self, config_path: Path, verbose: bool = False):
+ """Initialize validator with configuration"""
+ self.config_path = config_path
+ self.verbose = verbose
+ self.config = self._load_config()
+ self.result = ValidationResult()
+ self.project_root = Path.cwd()
+
+ def _load_config(self) -> Dict[str, Any]:
+ """Load validation rules from YAML config"""
+ if not self.config_path.exists():
+ print(f"❌ Configuration file not found: {self.config_path}")
+ sys.exit(1)
+
+ with open(self.config_path, 'r') as f:
+ config = yaml.safe_load(f)
+
+ print(f"📋 Loaded architecture rules: {config.get('project', 'unknown')}")
+ return config
+
+ def validate_all(self, target_path: Path = None) -> ValidationResult:
+ """Validate all files or specific path"""
+ print("\n🔍 Starting architecture validation...\n")
+
+ target = target_path or self.project_root
+
+ # Validate API endpoints
+ self._validate_api_endpoints(target)
+
+ # Validate service layer
+ self._validate_service_layer(target)
+
+ # Validate models
+ self._validate_models(target)
+
+ # Validate exception handling
+ self._validate_exceptions(target)
+
+ # Validate JavaScript
+ self._validate_javascript(target)
+
+ # Validate templates
+ self._validate_templates(target)
+
+ return self.result
+
+ def _validate_api_endpoints(self, target_path: Path):
+ """Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
+ print("📡 Validating API endpoints...")
+
+ api_files = list(target_path.glob("app/api/v1/**/*.py"))
+ self.result.files_checked += len(api_files)
+
+ for file_path in api_files:
+ if self._should_ignore_file(file_path):
+ continue
+
+ content = file_path.read_text()
+ lines = content.split('\n')
+
+ # API-001: Check for Pydantic model usage
+ self._check_pydantic_usage(file_path, content, lines)
+
+ # API-002: Check for business logic in endpoints
+ self._check_no_business_logic_in_endpoints(file_path, content, lines)
+
+ # API-003: Check exception handling
+ self._check_endpoint_exception_handling(file_path, content, lines)
+
+ # API-004: Check authentication
+ self._check_endpoint_authentication(file_path, content, lines)
+
+ def _check_pydantic_usage(self, file_path: Path, content: str, lines: List[str]):
+ """API-001: Ensure endpoints use Pydantic models"""
+ rule = self._get_rule("API-001")
+ if not rule:
+ return
+
+ # Check for response_model in route decorators
+ route_pattern = r'@router\.(get|post|put|delete|patch)'
+ dict_return_pattern = r'return\s+\{.*\}'
+
+ for i, line in enumerate(lines, 1):
+ # Check for dict returns in endpoints
+ if re.search(route_pattern, line):
+ # Look ahead for function body
+ func_start = i
+ indent = len(line) - len(line.lstrip())
+
+ # Find function body
+ for j in range(func_start, min(func_start + 20, len(lines))):
+ if j >= len(lines):
+ break
+
+ func_line = lines[j]
+ if re.search(dict_return_pattern, func_line):
+ self._add_violation(
+ rule_id="API-001",
+ rule_name=rule['name'],
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=j + 1,
+ message="Endpoint returns raw dict instead of Pydantic model",
+ context=func_line.strip(),
+ suggestion="Define a Pydantic response model and use response_model parameter"
+ )
+
+ def _check_no_business_logic_in_endpoints(self, file_path: Path, content: str, lines: List[str]):
+ """API-002: Ensure no business logic in endpoints"""
+ rule = self._get_rule("API-002")
+ if not rule:
+ return
+
+ anti_patterns = [
+ (r'db\.add\(', "Database operations should be in service layer"),
+ (r'db\.commit\(\)', "Database commits should be in service layer"),
+ (r'db\.query\(', "Database queries should be in service layer"),
+ (r'db\.execute\(', "Database operations should be in service layer"),
+ ]
+
+ for i, line in enumerate(lines, 1):
+ # Skip service method calls (allowed)
+ if '_service.' in line or 'service.' in line:
+ continue
+
+ for pattern, message in anti_patterns:
+ if re.search(pattern, line):
+ self._add_violation(
+ rule_id="API-002",
+ rule_name=rule['name'],
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=i,
+ message=message,
+ context=line.strip(),
+ suggestion="Move database operations to service layer"
+ )
+
+ def _check_endpoint_exception_handling(self, file_path: Path, content: str, lines: List[str]):
+ """API-003: Check proper exception handling in endpoints"""
+ rule = self._get_rule("API-003")
+ if not rule:
+ return
+
+ # Parse file to check for try/except in route handlers
+ try:
+ tree = ast.parse(content)
+ except SyntaxError:
+ return
+
+ for node in ast.walk(tree):
+ if isinstance(node, ast.FunctionDef):
+ # Check if it's a route handler
+ has_router_decorator = any(
+ isinstance(d, ast.Call) and
+ isinstance(d.func, ast.Attribute) and
+ getattr(d.func.value, 'id', None) == 'router'
+ for d in node.decorator_list
+ )
+
+ if has_router_decorator:
+ # Check if function body has try/except
+ has_try_except = any(
+ isinstance(child, ast.Try)
+ for child in ast.walk(node)
+ )
+
+ # Check if function calls service methods
+ has_service_call = any(
+ isinstance(child, ast.Call) and
+ isinstance(child.func, ast.Attribute) and
+ 'service' in getattr(child.func.value, 'id', '').lower()
+ for child in ast.walk(node)
+ )
+
+ if has_service_call and not has_try_except:
+ self._add_violation(
+ rule_id="API-003",
+ rule_name=rule['name'],
+ severity=Severity.WARNING,
+ file_path=file_path,
+ line_number=node.lineno,
+ message=f"Endpoint '{node.name}' calls service but lacks exception handling",
+ context=f"def {node.name}(...)",
+ suggestion="Wrap service calls in try/except and convert to HTTPException"
+ )
+
+ def _check_endpoint_authentication(self, file_path: Path, content: str, lines: List[str]):
+ """API-004: Check authentication on endpoints"""
+ rule = self._get_rule("API-004")
+ if not rule:
+ return
+
+ # This is a warning-level check
+ # Look for endpoints without Depends(get_current_*)
+ for i, line in enumerate(lines, 1):
+ if '@router.' in line and ('post' in line or 'put' in line or 'delete' in line):
+ # Check next 5 lines for auth
+ has_auth = False
+ for j in range(i, min(i + 5, len(lines))):
+ if 'Depends(get_current_' in lines[j]:
+ has_auth = True
+ break
+
+ if not has_auth and 'include_in_schema=False' not in ' '.join(lines[i:i+5]):
+ self._add_violation(
+ rule_id="API-004",
+ rule_name=rule['name'],
+ severity=Severity.WARNING,
+ file_path=file_path,
+ line_number=i,
+ message="Endpoint may be missing authentication",
+ context=line.strip(),
+ suggestion="Add Depends(get_current_user) or similar if endpoint should be protected"
+ )
+
+ def _validate_service_layer(self, target_path: Path):
+ """Validate service layer rules (SVC-001, SVC-002, SVC-003, SVC-004)"""
+ print("🔧 Validating service layer...")
+
+ service_files = list(target_path.glob("app/services/**/*.py"))
+ self.result.files_checked += len(service_files)
+
+ for file_path in service_files:
+ if self._should_ignore_file(file_path):
+ continue
+
+ content = file_path.read_text()
+ lines = content.split('\n')
+
+ # SVC-001: No HTTPException in services
+ self._check_no_http_exception_in_services(file_path, content, lines)
+
+ # SVC-002: Proper exception handling
+ self._check_service_exceptions(file_path, content, lines)
+
+ # SVC-003: DB session as parameter
+ self._check_db_session_parameter(file_path, content, lines)
+
+ def _check_no_http_exception_in_services(self, file_path: Path, content: str, lines: List[str]):
+ """SVC-001: Services must not raise HTTPException"""
+ rule = self._get_rule("SVC-001")
+ if not rule:
+ return
+
+ for i, line in enumerate(lines, 1):
+ if 'raise HTTPException' in line:
+ self._add_violation(
+ rule_id="SVC-001",
+ rule_name=rule['name'],
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=i,
+ message="Service raises HTTPException - use domain exceptions instead",
+ context=line.strip(),
+ suggestion="Create custom exception class (e.g., VendorNotFoundError) and raise that"
+ )
+
+ if 'from fastapi import HTTPException' in line or 'from fastapi.exceptions import HTTPException' in line:
+ self._add_violation(
+ rule_id="SVC-001",
+ rule_name=rule['name'],
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=i,
+ message="Service imports HTTPException - services should not know about HTTP",
+ context=line.strip(),
+ suggestion="Remove HTTPException import and use domain exceptions"
+ )
+
+ def _check_service_exceptions(self, file_path: Path, content: str, lines: List[str]):
+ """SVC-002: Check for proper exception handling"""
+ rule = self._get_rule("SVC-002")
+ if not rule:
+ return
+
+ for i, line in enumerate(lines, 1):
+ # Check for generic Exception raises
+ if re.match(r'\s*raise Exception\(', line):
+ self._add_violation(
+ rule_id="SVC-002",
+ rule_name=rule['name'],
+ severity=Severity.WARNING,
+ file_path=file_path,
+ line_number=i,
+ message="Service raises generic Exception - use specific domain exception",
+ context=line.strip(),
+ suggestion="Create custom exception class for this error case"
+ )
+
+ def _check_db_session_parameter(self, file_path: Path, content: str, lines: List[str]):
+ """SVC-003: Service methods should accept db session as parameter"""
+ rule = self._get_rule("SVC-003")
+ if not rule:
+ return
+
+ # Check for SessionLocal() creation in service files
+ for i, line in enumerate(lines, 1):
+ if 'SessionLocal()' in line and 'class' not in line:
+ self._add_violation(
+ rule_id="SVC-003",
+ rule_name=rule['name'],
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=i,
+ message="Service creates database session internally",
+ context=line.strip(),
+ suggestion="Accept db: Session as method parameter instead"
+ )
+
+ def _validate_models(self, target_path: Path):
+ """Validate model rules"""
+ print("📦 Validating models...")
+
+ model_files = list(target_path.glob("app/models/**/*.py"))
+ self.result.files_checked += len(model_files)
+
+ # Basic validation - can be extended
+ for file_path in model_files:
+ if self._should_ignore_file(file_path):
+ continue
+
+ content = file_path.read_text()
+ lines = content.split('\n')
+
+ # Check for mixing SQLAlchemy and Pydantic
+ for i, line in enumerate(lines, 1):
+ if re.search(r'class.*\(Base.*,.*BaseModel.*\)', line):
+ self._add_violation(
+ rule_id="MDL-002",
+ rule_name="Separate SQLAlchemy and Pydantic models",
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=i,
+ message="Model mixes SQLAlchemy Base and Pydantic BaseModel",
+ context=line.strip(),
+ suggestion="Keep SQLAlchemy models and Pydantic models separate"
+ )
+
+ def _validate_exceptions(self, target_path: Path):
+ """Validate exception handling patterns"""
+ print("⚠️ Validating exception handling...")
+
+ py_files = list(target_path.glob("**/*.py"))
+
+ for file_path in py_files:
+ if self._should_ignore_file(file_path):
+ continue
+
+ content = file_path.read_text()
+ lines = content.split('\n')
+
+ # EXC-002: Check for bare except
+ for i, line in enumerate(lines, 1):
+ if re.match(r'\s*except\s*:', line):
+ self._add_violation(
+ rule_id="EXC-002",
+ rule_name="No bare except clauses",
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=i,
+ message="Bare except clause catches all exceptions including system exits",
+ context=line.strip(),
+ suggestion="Specify exception type: except ValueError: or except Exception:"
+ )
+
+ def _validate_javascript(self, target_path: Path):
+ """Validate JavaScript patterns"""
+ print("🟨 Validating JavaScript...")
+
+ js_files = list(target_path.glob("static/admin/js/**/*.js"))
+ self.result.files_checked += len(js_files)
+
+ for file_path in js_files:
+ content = file_path.read_text()
+ lines = content.split('\n')
+
+ # JS-001: Check for window.apiClient
+ for i, line in enumerate(lines, 1):
+ if 'window.apiClient' in line and '//' not in line[:line.find('window.apiClient')] if 'window.apiClient' in line else True:
+ self._add_violation(
+ rule_id="JS-001",
+ rule_name="Use apiClient directly",
+ severity=Severity.WARNING,
+ file_path=file_path,
+ line_number=i,
+ message="Use apiClient directly instead of window.apiClient",
+ context=line.strip(),
+ suggestion="Replace window.apiClient with apiClient"
+ )
+
+ # JS-002: Check for console usage
+ for i, line in enumerate(lines, 1):
+ if re.search(r'console\.(log|warn|error)', line):
+ # Skip if it's a comment or bootstrap message
+ if '//' in line or '✅' in line or 'eslint-disable' in line:
+ continue
+
+ self._add_violation(
+ rule_id="JS-002",
+ rule_name="Use centralized logger",
+ severity=Severity.WARNING,
+ file_path=file_path,
+ line_number=i,
+ message="Use centralized logger instead of console",
+ context=line.strip()[:80],
+ suggestion="Use window.LogConfig.createLogger('moduleName')"
+ )
+
+ def _validate_templates(self, target_path: Path):
+ """Validate template patterns"""
+ print("📄 Validating templates...")
+
+ template_files = list(target_path.glob("app/templates/admin/**/*.html"))
+ self.result.files_checked += len(template_files)
+
+ for file_path in template_files:
+ # Skip base template and partials
+ if 'base.html' in file_path.name or 'partials' in str(file_path):
+ continue
+
+ content = file_path.read_text()
+ lines = content.split('\n')
+
+ # TPL-001: Check for extends
+ has_extends = any('{% extends' in line and 'admin/base.html' in line for line in lines)
+
+ if not has_extends:
+ self._add_violation(
+ rule_id="TPL-001",
+ rule_name="Templates must extend base",
+ severity=Severity.ERROR,
+ file_path=file_path,
+ line_number=1,
+ message="Admin template does not extend admin/base.html",
+ context=file_path.name,
+ suggestion="Add {% extends 'admin/base.html' %} at the top"
+ )
+
+ def _get_rule(self, rule_id: str) -> Dict[str, Any]:
+ """Get rule configuration by ID"""
+ # Look in different rule categories
+ for category in ['api_endpoint_rules', 'service_layer_rules', 'model_rules',
+ 'exception_rules', 'javascript_rules', 'template_rules']:
+ rules = self.config.get(category, [])
+ for rule in rules:
+ if rule.get('id') == rule_id:
+ return rule
+ return None
+
+ def _should_ignore_file(self, file_path: Path) -> bool:
+ """Check if file should be ignored"""
+ ignore_patterns = self.config.get('ignore', {}).get('files', [])
+
+ for pattern in ignore_patterns:
+ if file_path.match(pattern):
+ return True
+
+ return False
+
+ def _add_violation(self, rule_id: str, rule_name: str, severity: Severity,
+ file_path: Path, line_number: int, message: str,
+ context: str = "", suggestion: str = ""):
+ """Add a violation to results"""
+ violation = Violation(
+ rule_id=rule_id,
+ rule_name=rule_name,
+ severity=severity,
+ file_path=file_path,
+ line_number=line_number,
+ message=message,
+ context=context,
+ suggestion=suggestion
+ )
+ self.result.violations.append(violation)
+
+ def print_report(self):
+ """Print validation report"""
+ print("\n" + "=" * 80)
+ print("📊 ARCHITECTURE VALIDATION REPORT")
+ print("=" * 80 + "\n")
+
+ print(f"Files checked: {self.result.files_checked}")
+ print(f"Total violations: {len(self.result.violations)}\n")
+
+ # Group by severity
+ errors = [v for v in self.result.violations if v.severity == Severity.ERROR]
+ warnings = [v for v in self.result.violations if v.severity == Severity.WARNING]
+
+ if errors:
+ print(f"\n❌ ERRORS ({len(errors)}):")
+ print("-" * 80)
+ for violation in errors:
+ self._print_violation(violation)
+
+ if warnings:
+ print(f"\n⚠️ WARNINGS ({len(warnings)}):")
+ print("-" * 80)
+ for violation in warnings:
+ self._print_violation(violation)
+
+ # Summary
+ print("\n" + "=" * 80)
+ if self.result.has_errors():
+ print("❌ VALIDATION FAILED - Fix errors before committing")
+ print("=" * 80)
+ return 1
+ elif self.result.has_warnings():
+ print("⚠️ VALIDATION PASSED WITH WARNINGS")
+ print("=" * 80)
+ return 0
+ else:
+ print("✅ VALIDATION PASSED - No violations found")
+ print("=" * 80)
+ return 0
+
+ def _print_violation(self, v: Violation):
+ """Print a single violation"""
+ rel_path = v.file_path.relative_to(self.project_root) if self.project_root in v.file_path.parents else v.file_path
+
+ print(f"\n [{v.rule_id}] {v.rule_name}")
+ print(f" File: {rel_path}:{v.line_number}")
+ print(f" Issue: {v.message}")
+
+ if v.context and self.verbose:
+ print(f" Context: {v.context}")
+
+ if v.suggestion:
+ print(f" 💡 Suggestion: {v.suggestion}")
+
+
+def main():
+ """Main entry point"""
+ parser = argparse.ArgumentParser(
+ description="Validate architecture patterns in codebase",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__
+ )
+
+ parser.add_argument(
+ 'path',
+ nargs='?',
+ type=Path,
+ default=Path.cwd(),
+ help="Path to validate (default: current directory)"
+ )
+
+ parser.add_argument(
+ '-c', '--config',
+ type=Path,
+ default=Path.cwd() / '.architecture-rules.yaml',
+ help="Path to architecture rules config (default: .architecture-rules.yaml)"
+ )
+
+ parser.add_argument(
+ '-v', '--verbose',
+ action='store_true',
+ help="Show detailed output including context"
+ )
+
+ parser.add_argument(
+ '--errors-only',
+ action='store_true',
+ help="Only show errors, suppress warnings"
+ )
+
+ args = parser.parse_args()
+
+ # Create validator
+ validator = ArchitectureValidator(args.config, verbose=args.verbose)
+
+ # Run validation
+ result = validator.validate_all(args.path)
+
+ # Print report
+ exit_code = validator.print_report()
+
+ sys.exit(exit_code)
+
+
+if __name__ == '__main__':
+ main()