Move 9 init/seed scripts into scripts/seed/ and 7 validation scripts (+ validators/ subfolder) into scripts/validate/ to reduce clutter in the root scripts/ directory. Update all references across Makefile, CI/CD configs, pre-commit hooks, docs (~40 files), and Python imports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1138 lines
28 KiB
Markdown
1138 lines
28 KiB
Markdown
# Architecture Rules Reference
|
|
|
|
This document provides a comprehensive reference for all architectural rules enforced by the `scripts/validate/validate_architecture.py` validator.
|
|
|
|
## Overview
|
|
|
|
The architecture validator ensures consistent patterns, separation of concerns, and best practices across the entire codebase. Rules are organized by category and severity level.
|
|
|
|
**Version:** 2.0
|
|
**Total Rules:** 50+
|
|
**Configuration File:** `.architecture-rules.yaml`
|
|
|
|
## Running the Validator
|
|
|
|
### Using Make Commands (Recommended)
|
|
|
|
```bash
|
|
# Check all files
|
|
make arch-check
|
|
|
|
# Check a single file
|
|
make arch-check-file file="app/api/v1/admin/stores.py"
|
|
|
|
# Check all files related to an entity (merchant, store, user, etc.)
|
|
make arch-check-object name="merchant"
|
|
make arch-check-object name="store"
|
|
|
|
# Full QA (includes arch-check)
|
|
make qa
|
|
```
|
|
|
|
### Using Python Directly
|
|
|
|
```bash
|
|
# Check all files
|
|
python scripts/validate/validate_architecture.py
|
|
|
|
# Check specific directory
|
|
python scripts/validate/validate_architecture.py -d app/api/
|
|
|
|
# Check a single file
|
|
python scripts/validate/validate_architecture.py -f app/api/v1/admin/stores.py
|
|
|
|
# Check all files for an entity
|
|
python scripts/validate/validate_architecture.py -o merchant
|
|
python scripts/validate/validate_architecture.py -o store
|
|
|
|
# Verbose output
|
|
python scripts/validate/validate_architecture.py --verbose
|
|
|
|
# JSON output (for CI/CD)
|
|
python scripts/validate/validate_architecture.py --json
|
|
```
|
|
|
|
### Output Format
|
|
|
|
The validator displays a summary table for validated files:
|
|
|
|
```
|
|
📋 FILE SUMMARY:
|
|
--------------------------------------------------------------------------------
|
|
File Status Errors Warnings
|
|
----------------------------- -------- ------- --------
|
|
app/api/v1/admin/merchants.py ❌ FAILED 6 9
|
|
app/services/merchant_service.py ✅ PASSED 0 0
|
|
models/database/merchant.py ✅ PASSED 0 0
|
|
--------------------------------------------------------------------------------
|
|
Total: 3 files | ✅ 2 passed | ❌ 1 failed | 6 errors | 9 warnings
|
|
```
|
|
|
|
## Severity Levels
|
|
|
|
| Severity | Description | Exit Code | Action Required |
|
|
|----------|-------------|-----------|-----------------|
|
|
| **Error** | Critical architectural violation | 1 | Must fix before committing |
|
|
| **Warning** | Pattern deviation | 0 | Should fix, doesn't block |
|
|
| **Info** | Suggestion for improvement | 0 | Optional improvement |
|
|
|
|
---
|
|
|
|
## Core Architectural Principles
|
|
|
|
1. **Separation of Concerns** - API endpoints handle HTTP, services handle business logic
|
|
2. **Layered Architecture** - Routes → Services → Models
|
|
3. **Type Safety** - Pydantic for API, SQLAlchemy for database
|
|
4. **Proper Exception Handling** - Domain exceptions in services, HTTPException in routes
|
|
5. **Multi-Tenancy** - All queries scoped to store_id
|
|
6. **Consistent Naming** - API files: plural, Services: singular+service, Models: singular
|
|
|
|
---
|
|
|
|
## Backend Rules
|
|
|
|
### API Endpoint Rules (app/api/v1/**/*.py)
|
|
|
|
#### API-001: Use Pydantic Models for Request/Response
|
|
**Severity:** Error
|
|
|
|
All API endpoints must use Pydantic models (BaseModel) for request bodies and response models. Never use raw dicts or SQLAlchemy models directly.
|
|
|
|
```python
|
|
# ✅ Good
|
|
@router.post("/stores", response_model=StoreResponse)
|
|
async def create_store(store: StoreCreate):
|
|
return store_service.create_store(db, store)
|
|
|
|
# ❌ Bad
|
|
@router.post("/stores")
|
|
async def create_store(data: dict):
|
|
return {"name": data["name"]} # No validation!
|
|
```
|
|
|
|
#### API-002: No Business Logic in Endpoints
|
|
**Severity:** Error
|
|
|
|
API endpoints should only handle HTTP concerns (validation, auth, response formatting). All business logic must be delegated to service layer.
|
|
|
|
```python
|
|
# ✅ Good
|
|
@router.post("/stores")
|
|
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
|
|
result = store_service.create_store(db, store)
|
|
return result
|
|
|
|
# ❌ Bad
|
|
@router.post("/stores")
|
|
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
|
|
db_store = Store(name=store.name)
|
|
db.add(db_store) # Business logic in endpoint!
|
|
db.commit()
|
|
return db_store
|
|
```
|
|
|
|
**Anti-patterns detected:**
|
|
- `db.add(`
|
|
- `db.commit()`
|
|
- `db.query(`
|
|
|
|
#### API-003: No HTTPException in Endpoints
|
|
**Severity:** Error
|
|
|
|
API endpoints must NOT raise HTTPException directly. Instead, let domain exceptions bubble up to the global exception handler which converts them to appropriate HTTP responses.
|
|
|
|
```python
|
|
# ✅ Good - Let domain exceptions bubble up
|
|
@router.post("/stores")
|
|
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
|
|
# Service raises StoreAlreadyExistsException if duplicate
|
|
# Global handler converts to 409 Conflict
|
|
return store_service.create_store(db, store)
|
|
|
|
# ❌ Bad - Don't raise HTTPException directly
|
|
@router.post("/stores")
|
|
async def create_store(store: StoreCreate, db: Session = Depends(get_db)):
|
|
if store_service.exists(db, store.subdomain):
|
|
raise HTTPException(status_code=409, detail="Store exists") # BAD!
|
|
return store_service.create_store(db, store)
|
|
```
|
|
|
|
**Pattern:** Services raise domain exceptions → Global handler converts to HTTP responses
|
|
|
|
#### API-004: Proper Authentication
|
|
**Severity:** Warning
|
|
|
|
Protected endpoints must use Depends() for authentication.
|
|
|
|
```python
|
|
# ✅ Good - Protected endpoint with authentication
|
|
@router.post("/stores")
|
|
async def create_store(
|
|
store: StoreCreate,
|
|
current_user: User = Depends(get_current_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
pass
|
|
```
|
|
|
|
**Auto-Excluded Files:**
|
|
|
|
The validator automatically skips API-004 checks for authentication endpoint files (`*/auth.py`) since login, logout, and registration endpoints are intentionally public.
|
|
|
|
**Marking Public Endpoints:**
|
|
|
|
For other intentionally public endpoints (webhooks, health checks, etc.), use a comment marker:
|
|
|
|
```python
|
|
# ✅ Good - Webhook endpoint marked as public
|
|
# public - Stripe webhook receives external callbacks
|
|
@router.post("/webhook/stripe")
|
|
def stripe_webhook(request: Request):
|
|
...
|
|
|
|
# ✅ Good - Using noqa style
|
|
@router.post("/health") # noqa: API-004
|
|
def health_check():
|
|
...
|
|
```
|
|
|
|
**Recognized markers:**
|
|
- `# public` - descriptive marker for intentionally unauthenticated endpoints
|
|
- `# noqa: API-004` - standard noqa style to suppress the warning
|
|
|
|
#### API-005: Multi-Tenant Scoping
|
|
**Severity:** Error
|
|
|
|
All queries in store/shop contexts must filter by store_id.
|
|
|
|
---
|
|
|
|
### Service Layer Rules (app/services/**/*.py)
|
|
|
|
#### SVC-001: No HTTPException in Services
|
|
**Severity:** Error
|
|
|
|
Services are business logic layer - they should NOT know about HTTP. Raise domain-specific exceptions instead.
|
|
|
|
```python
|
|
# ✅ Good
|
|
class StoreService:
|
|
def create_store(self, db: Session, store_data):
|
|
if self._store_exists(db, store_data.subdomain):
|
|
raise StoreAlreadyExistsError(f"Store {store_data.subdomain} exists")
|
|
|
|
# ❌ Bad
|
|
class StoreService:
|
|
def create_store(self, db: Session, store_data):
|
|
if self._store_exists(db, store_data.subdomain):
|
|
raise HTTPException(status_code=409, detail="Store exists") # BAD!
|
|
```
|
|
|
|
#### SVC-002: Use Proper Exception Handling
|
|
**Severity:** Error
|
|
|
|
Services should raise meaningful domain exceptions, not generic Exception.
|
|
|
|
```python
|
|
# ✅ Good
|
|
class StoreAlreadyExistsError(Exception):
|
|
pass
|
|
|
|
def create_store(self, db: Session, store_data):
|
|
if self._store_exists(db, store_data.subdomain):
|
|
raise StoreAlreadyExistsError(f"Subdomain {store_data.subdomain} taken")
|
|
|
|
# ❌ Bad
|
|
def create_store(self, db: Session, store_data):
|
|
if self._store_exists(db, store_data.subdomain):
|
|
raise Exception("Store exists") # Too generic!
|
|
```
|
|
|
|
#### SVC-003: Accept DB Session as Parameter
|
|
**Severity:** Error
|
|
|
|
Service methods should receive database session as a parameter for testability and transaction control.
|
|
|
|
```python
|
|
# ✅ Good
|
|
def create_store(self, db: Session, store_data: StoreCreate):
|
|
store = Store(**store_data.dict())
|
|
db.add(store)
|
|
db.commit()
|
|
return store
|
|
|
|
# ❌ Bad
|
|
def create_store(self, store_data: StoreCreate):
|
|
db = SessionLocal() # Don't create session inside!
|
|
store = Store(**store_data.dict())
|
|
db.add(store)
|
|
db.commit()
|
|
```
|
|
|
|
#### SVC-004: Use Pydantic Models for Input
|
|
**Severity:** Warning
|
|
|
|
Service methods should accept Pydantic models for complex inputs to ensure type safety.
|
|
|
|
#### SVC-005: Scope Queries to store_id
|
|
**Severity:** Error
|
|
|
|
All database queries must be scoped to store_id to prevent cross-tenant data access.
|
|
|
|
---
|
|
|
|
### Model Rules (models/**/*.py)
|
|
|
|
#### MDL-001: Database Models Use SQLAlchemy Base
|
|
**Severity:** Error
|
|
|
|
All database models must inherit from SQLAlchemy Base.
|
|
|
|
#### MDL-002: Never Mix SQLAlchemy and Pydantic
|
|
**Severity:** Error
|
|
|
|
```python
|
|
# ❌ Bad
|
|
class Product(Base, BaseModel): # NEVER DO THIS!
|
|
pass
|
|
```
|
|
|
|
Keep them separate:
|
|
- `models/database/product.py` - SQLAlchemy models
|
|
- `models/schema/product.py` - Pydantic models
|
|
|
|
#### MDL-003: Use from_attributes in Pydantic
|
|
**Severity:** Error
|
|
|
|
Pydantic response models must enable `from_attributes` to work with SQLAlchemy models.
|
|
|
|
```python
|
|
# ✅ Good
|
|
class ProductResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
```
|
|
|
|
#### MDL-004: Use Singular Table Names
|
|
**Severity:** Warning
|
|
|
|
Database table names should be singular lowercase (e.g., 'store' not 'stores').
|
|
|
|
---
|
|
|
|
### Exception Handling Rules
|
|
|
|
#### EXC-001: Define Custom Exceptions
|
|
**Severity:** Warning
|
|
|
|
Create domain-specific exceptions in `app/exceptions/` for better error handling.
|
|
|
|
```python
|
|
# ✅ Good - app/exceptions/store.py
|
|
class StoreError(Exception):
|
|
"""Base exception for store-related errors"""
|
|
pass
|
|
|
|
class StoreNotFoundError(StoreError):
|
|
pass
|
|
|
|
class StoreAlreadyExistsError(StoreError):
|
|
pass
|
|
```
|
|
|
|
#### EXC-002: Never Use Bare Except
|
|
**Severity:** Error
|
|
|
|
```python
|
|
# ❌ Bad
|
|
try:
|
|
result = service.do_something()
|
|
except: # Too broad!
|
|
pass
|
|
|
|
# ✅ Good
|
|
try:
|
|
result = service.do_something()
|
|
except ValueError as e:
|
|
logger.error(f"Validation error: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error: {e}")
|
|
```
|
|
|
|
#### EXC-003: Log Exceptions with Context
|
|
**Severity:** Warning
|
|
|
|
When catching exceptions, log them with context and stack trace.
|
|
|
|
```python
|
|
# ✅ Good
|
|
try:
|
|
result = service.process()
|
|
except Exception as e:
|
|
logger.error(f"Processing failed: {e}", exc_info=True)
|
|
```
|
|
|
|
---
|
|
|
|
### Naming Convention Rules
|
|
|
|
#### NAM-001: API Files Use PLURAL Names
|
|
**Severity:** Error
|
|
|
|
```
|
|
✅ app/api/v1/admin/stores.py
|
|
✅ app/api/v1/admin/products.py
|
|
❌ app/api/v1/admin/store.py
|
|
```
|
|
|
|
Exceptions: `__init__.py`, `auth.py`, `health.py`
|
|
|
|
#### NAM-002: Service Files Use SINGULAR + service
|
|
**Severity:** Error
|
|
|
|
```
|
|
✅ app/services/store_service.py
|
|
✅ app/services/product_service.py
|
|
❌ app/services/stores_service.py
|
|
```
|
|
|
|
#### NAM-003: Model Files Use SINGULAR Names
|
|
**Severity:** Error
|
|
|
|
```
|
|
✅ models/database/product.py
|
|
✅ models/schema/store.py
|
|
❌ models/database/products.py
|
|
```
|
|
|
|
#### NAM-004: Use 'store' not 'shop'
|
|
**Severity:** Warning
|
|
|
|
Use consistent terminology: 'store' for shop owners, 'shop' only for customer-facing frontend.
|
|
|
|
```python
|
|
# ✅ Good
|
|
store_id
|
|
store_service
|
|
|
|
# ❌ Bad
|
|
shop_id
|
|
shop_service
|
|
```
|
|
|
|
#### NAM-005: Use 'inventory' not 'stock'
|
|
**Severity:** Warning
|
|
|
|
```python
|
|
# ✅ Good
|
|
inventory_service
|
|
inventory_level
|
|
|
|
# ❌ Bad
|
|
stock_service
|
|
stock_level
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Rules
|
|
|
|
### JavaScript Rules (static/**/js/**/*.js)
|
|
|
|
#### JS-001: Use Centralized Logger
|
|
**Severity:** Error
|
|
|
|
Never use console.log, console.error, console.warn directly. Use window.LogConfig.createLogger().
|
|
|
|
```javascript
|
|
// ✅ Good
|
|
const pageLog = window.LogConfig.createLogger('dashboard');
|
|
pageLog.info('Dashboard loaded');
|
|
pageLog.error('Failed to load data', error);
|
|
|
|
// ❌ Bad
|
|
console.log('Dashboard loaded');
|
|
console.error('Failed to load data', error);
|
|
```
|
|
|
|
Exceptions: Bootstrap messages with `console.log('✅'` allowed.
|
|
|
|
#### JS-002: Use Lowercase apiClient
|
|
**Severity:** Error
|
|
|
|
```javascript
|
|
// ✅ Good
|
|
await apiClient.get('/api/v1/stores');
|
|
await apiClient.post('/api/v1/products', data);
|
|
|
|
// ❌ Bad
|
|
await ApiClient.get('/api/v1/stores');
|
|
await API_CLIENT.post('/api/v1/products', data);
|
|
```
|
|
|
|
#### JS-003: Alpine Components Must Inherit ...data()
|
|
**Severity:** Error
|
|
|
|
All Alpine.js components must inherit base layout data using spread operator.
|
|
|
|
```javascript
|
|
// ✅ Good
|
|
function adminDashboard() {
|
|
return {
|
|
...data(), // Inherit base layout data
|
|
currentPage: 'dashboard',
|
|
// ... component-specific data
|
|
};
|
|
}
|
|
|
|
// ❌ Bad
|
|
function adminDashboard() {
|
|
return {
|
|
currentPage: 'dashboard',
|
|
// Missing ...data()!
|
|
};
|
|
}
|
|
```
|
|
|
|
#### JS-004: Set currentPage in Components
|
|
**Severity:** Error
|
|
|
|
All Alpine.js page components must set a currentPage identifier.
|
|
|
|
```javascript
|
|
// ✅ Good
|
|
return {
|
|
...data(),
|
|
currentPage: 'dashboard', // Required!
|
|
// ...
|
|
};
|
|
```
|
|
|
|
#### JS-005: Initialization Guards
|
|
**Severity:** Error
|
|
|
|
Init methods should prevent duplicate initialization.
|
|
|
|
```javascript
|
|
// ✅ Good
|
|
init() {
|
|
if (window._pageInitialized) return;
|
|
window._pageInitialized = true;
|
|
|
|
// Initialization logic...
|
|
}
|
|
```
|
|
|
|
#### JS-006: Error Handling for Async Operations
|
|
**Severity:** Error
|
|
|
|
All API calls and async operations must have error handling.
|
|
|
|
```javascript
|
|
// ✅ Good
|
|
async loadData() {
|
|
try {
|
|
const response = await apiClient.get('/api/v1/data');
|
|
this.items = response.data;
|
|
} catch (error) {
|
|
window.LogConfig.logError(error, 'Load Data');
|
|
Utils.showToast('Failed to load data', 'error');
|
|
}
|
|
}
|
|
```
|
|
|
|
#### JS-007: Set Loading State
|
|
**Severity:** Warning
|
|
|
|
Loading state should be set before and cleared after async operations.
|
|
|
|
```javascript
|
|
// ✅ Good
|
|
async loadData() {
|
|
this.loading = true;
|
|
try {
|
|
const response = await apiClient.get('/api/v1/data');
|
|
this.items = response.data;
|
|
} catch (error) {
|
|
// Error handling
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Template Rules (app/templates/**/*.html)
|
|
|
|
#### TPL-001: Admin Templates Extend admin/base.html
|
|
**Severity:** Error
|
|
|
|
All admin templates must extend the base template for consistent layout (sidebar, navigation, etc.).
|
|
|
|
```jinja
|
|
{# ✅ Good - Extends base template #}
|
|
{% extends "admin/base.html" %}
|
|
{% block content %}
|
|
...
|
|
{% endblock %}
|
|
```
|
|
|
|
**Auto-Excluded Files:**
|
|
|
|
The validator automatically skips TPL-001 checks for:
|
|
|
|
- `login.html` - Standalone login page (no sidebar/navigation needed)
|
|
- `errors/*.html` - Error pages extend `errors/base.html` instead
|
|
- `test-*.html` - Test/development templates
|
|
- `base.html` - The base template itself
|
|
- `partials/*.html` - Partial templates included in other templates
|
|
|
|
**Marking Standalone Templates:**
|
|
|
|
For other templates that intentionally don't extend base.html, use a comment marker in the first 5 lines:
|
|
|
|
```jinja
|
|
{# standalone - Minimal monitoring page without admin chrome #}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
...
|
|
```
|
|
|
|
**Recognized markers:**
|
|
- `{# standalone #}` - Jinja comment style
|
|
- `{# noqa: TPL-001 #}` - Standard noqa style
|
|
- `<!-- standalone -->` - HTML comment style
|
|
|
|
#### TPL-002: Store Templates Extend store/base.html
|
|
**Severity:** Error
|
|
|
|
```jinja
|
|
✅ {% extends "store/base.html" %}
|
|
```
|
|
|
|
#### TPL-003: Shop Templates Extend shop/base.html
|
|
**Severity:** Error
|
|
|
|
```jinja
|
|
✅ {% extends "shop/base.html" %}
|
|
```
|
|
|
|
#### TPL-004: Use x-text for Dynamic Content
|
|
**Severity:** Warning
|
|
|
|
Use x-text directive for dynamic content to prevent XSS vulnerabilities.
|
|
|
|
```html
|
|
✅ <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.
|
|
|
|
```html
|
|
✅ <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.
|
|
|
|
```html
|
|
✅ <div x-show="loading">Loading...</div>
|
|
```
|
|
|
|
#### TPL-007: Implement Empty State
|
|
**Severity:** Warning
|
|
|
|
Show empty state when lists have no items.
|
|
|
|
```html
|
|
✅ <template x-if="items.length === 0">
|
|
<p>No items found</p>
|
|
</template>
|
|
```
|
|
|
|
#### TPL-008: Use Valid Block Names
|
|
**Severity:** Error
|
|
|
|
Templates must use block names that exist in their base template. Using undefined blocks silently fails (content is not rendered).
|
|
|
|
**Valid Admin Template Blocks:**
|
|
- `title` - Page title
|
|
- `extra_head` - Additional head content (CSS, meta tags)
|
|
- `alpine_data` - Alpine.js component function name
|
|
- `content` - Main page content
|
|
- `extra_scripts` - Additional JavaScript
|
|
|
|
```jinja
|
|
{# ✅ Good - Valid block names #}
|
|
{% block extra_scripts %}
|
|
<script src="/static/admin/js/dashboard.js"></script>
|
|
{% endblock %}
|
|
|
|
{# ❌ Bad - Invalid block name (silently ignored!) #}
|
|
{% block page_scripts %}
|
|
<script src="/static/admin/js/dashboard.js"></script>
|
|
{% endblock %}
|
|
```
|
|
|
|
**Common mistakes detected:**
|
|
- `page_scripts` → use `extra_scripts`
|
|
- `scripts` → use `extra_scripts`
|
|
- `js` → use `extra_scripts`
|
|
- `head` → use `extra_head`
|
|
|
|
---
|
|
|
|
### Jinja Macro Rules (app/templates/**/*.html)
|
|
|
|
#### MAC-001: Use Shared Macros for Repeated Patterns
|
|
**Severity:** Error
|
|
|
|
Never copy-paste HTML patterns. Use or create shared macros in `app/templates/shared/macros/`.
|
|
|
|
```jinja
|
|
{# ✅ Good - Using shared macro #}
|
|
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
|
|
{% call table_wrapper() %}
|
|
{{ table_header(['Name', 'Email', 'Status']) }}
|
|
<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 %}`.
|
|
|
|
```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 `<input type="number">` 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 #}
|
|
<input type="number" x-model="quantity" min="1" max="99" />
|
|
```
|
|
|
|
**Suppress with `noqa` for ID fields:**
|
|
```jinja
|
|
{# 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.
|
|
|
|
```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 store-specific theming.
|
|
|
|
```html
|
|
✅ <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.
|
|
|
|
```html
|
|
✅ class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
|
|
❌ class="grid grid-cols-4"
|
|
```
|
|
|
|
---
|
|
|
|
## Module Structure Rules
|
|
|
|
Module rules enforce consistent structure and completeness across all modules in `app/modules/`.
|
|
|
|
### MOD-020: Module Definition Completeness
|
|
**Severity:** Warning
|
|
|
|
Module definitions should include required attributes: code, name, description, version, and features.
|
|
|
|
```python
|
|
# ✅ Good - Complete definition
|
|
module = ModuleDefinition(
|
|
code="billing",
|
|
name="Billing & Subscriptions",
|
|
description="Platform subscription management",
|
|
version="1.0.0",
|
|
features=["subscription_management", "billing_history"],
|
|
permissions=[...],
|
|
)
|
|
|
|
# ❌ Bad - Missing features
|
|
module = ModuleDefinition(
|
|
code="billing",
|
|
name="Billing",
|
|
description="...",
|
|
version="1.0.0",
|
|
# Missing features and permissions
|
|
)
|
|
```
|
|
|
|
### MOD-021: Modules with Menus Should Have Features
|
|
**Severity:** Warning
|
|
|
|
If a module defines menu items or menu sections, it should also define features.
|
|
|
|
```python
|
|
# ❌ Bad - Has menus but no features
|
|
module = ModuleDefinition(
|
|
code="billing",
|
|
menus={FrontendType.ADMIN: [...]},
|
|
# Missing features!
|
|
)
|
|
```
|
|
|
|
### MOD-022: Feature Modules Should Have Permissions
|
|
**Severity:** Info
|
|
|
|
Modules with features should define permissions for RBAC, unless:
|
|
- `is_internal=True` (internal tools)
|
|
- Storefront-only module (session-based, no admin UI)
|
|
|
|
```python
|
|
# ✅ Good - Features with permissions
|
|
module = ModuleDefinition(
|
|
code="billing",
|
|
features=["subscription_management"],
|
|
permissions=[
|
|
PermissionDefinition(
|
|
id="billing.view_subscriptions",
|
|
label_key="billing.permissions.view_subscriptions",
|
|
description_key="billing.permissions.view_subscriptions_desc",
|
|
category="billing",
|
|
),
|
|
],
|
|
)
|
|
```
|
|
|
|
### MOD-023: Router Pattern Consistency
|
|
**Severity:** Info
|
|
|
|
Modules with routers should use the `get_*_with_routers()` pattern for lazy imports.
|
|
|
|
```python
|
|
# ✅ Good - Lazy router pattern
|
|
def _get_admin_router():
|
|
from app.modules.billing.routes.api.admin import admin_router
|
|
return admin_router
|
|
|
|
def get_billing_module_with_routers() -> ModuleDefinition:
|
|
billing_module.admin_router = _get_admin_router()
|
|
return billing_module
|
|
```
|
|
|
|
See [Module System Architecture](../architecture/module-system.md) for complete MOD-001 to MOD-019 rules.
|
|
|
|
---
|
|
|
|
## Security & Multi-Tenancy Rules
|
|
|
|
### Multi-Tenancy Rules
|
|
|
|
#### MT-001: Scope All Queries to store_id
|
|
**Severity:** Error
|
|
|
|
In store/shop contexts, all database queries must filter by store_id.
|
|
|
|
```python
|
|
# ✅ Good
|
|
def get_products(self, db: Session, store_id: int):
|
|
return db.query(Product).filter(Product.store_id == store_id).all()
|
|
|
|
# ❌ Bad
|
|
def get_products(self, db: Session):
|
|
return db.query(Product).all() # Cross-tenant data leak!
|
|
```
|
|
|
|
#### MT-002: No Cross-Store Data Access
|
|
**Severity:** Error
|
|
|
|
Queries must never access data from other stores.
|
|
|
|
---
|
|
|
|
### 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/store/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: Store Context Injection
|
|
**Severity:** Error
|
|
|
|
Store context middleware must set `request.state.store_id` and `request.state.store`.
|
|
|
|
---
|
|
|
|
## 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/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 |
|
|
| Missing currentPage | Add `currentPage: 'page-name'` in component return |
|
|
| Invalid block name | Use valid block: `extra_scripts`, `extra_head`, etc. |
|
|
| 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/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 | Info |
|
|
|----------|-------|--------|----------|------|
|
|
| Backend | 20 | 15 | 5 | 0 |
|
|
| Module Structure | 23 | 7 | 10 | 6 |
|
|
| Frontend JS | 7 | 6 | 1 | 0 |
|
|
| Frontend Templates | 8 | 4 | 4 | 0 |
|
|
| Frontend Macros | 5 | 2 | 3 | 0 |
|
|
| Frontend Components | 1 | 0 | 1 | 0 |
|
|
| Frontend Styling | 4 | 1 | 3 | 0 |
|
|
| Naming | 5 | 3 | 2 | 0 |
|
|
| Security | 5 | 5 | 0 | 0 |
|
|
| Quality | 3 | 2 | 1 | 0 |
|
|
| **Total** | **81** | **45** | **30** | **6** |
|
|
|
|
---
|
|
|
|
**Last Updated:** 2026-02-02
|
|
**Version:** 2.5
|