Files
orion/.architecture-rules/module.yaml
Samir Boulahtit e77535e2cd docs: add UserContext pattern documentation and architecture rules
Documentation:
- docs/architecture/user-context-pattern.md: Comprehensive guide on
  UserContext vs User model, JWT token mapping, common mistakes

Architecture Rules (auth.yaml):
- AUTH-005: Routes must use UserContext, not User model attributes
- AUTH-006: JWT token context fields must be defined in UserContext
- AUTH-007: Response models must match available UserContext data

Architecture Rules (module.yaml):
- MOD-024: Module static file mount order - specific paths first

These rules prevent issues like:
- Accessing SQLAlchemy relationships on Pydantic schemas
- Missing token fields causing fallback warnings
- Response model validation errors from missing timestamps
- 404 errors for module locale files due to mount order

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 22:35:04 +01:00

764 lines
26 KiB
YAML

# Architecture Rules - Module Structure Rules
# Rules for app/modules/*/ directories
module_rules:
- id: "MOD-001"
name: "Self-contained modules must have required directories"
severity: "error"
description: |
When a module declares is_self_contained=True in its definition,
it must have the following directory structure:
Required directories:
- services/ - Business logic services (actual code, not re-exports)
- models/ - Database models (actual code, not re-exports)
- schemas/ - Pydantic schemas (actual code, not re-exports)
- routes/ - API and page routes
- routes/api/ - API endpoints
- routes/pages/ - Page endpoints (if module has UI)
Optional directories (based on module needs):
- templates/ - Jinja templates (if module has UI)
- static/ - Static assets (JS/CSS)
- locales/ - Translation files
- tasks/ - Celery tasks
- exceptions.py - Module-specific exceptions
WHY THIS MATTERS:
- Consistency: All modules follow the same structure
- Discoverability: Developers know where to find code
- Encapsulation: Module owns all its components
- Testability: Clear boundaries for unit tests
pattern:
directory_pattern: "app/modules/*/"
required_when: "is_self_contained=True"
required_dirs:
- "services/"
- "models/"
- "schemas/"
- "routes/"
- "routes/api/"
- id: "MOD-002"
name: "Module services must contain actual code, not re-exports"
severity: "warning"
description: |
Self-contained module services should contain actual business logic,
not just re-exports from legacy locations.
If a module's services/ directory only contains re-exports (from ...),
the module is not truly self-contained.
WRONG (re-export only):
# app/modules/analytics/services/stats_service.py
from app.services.stats_service import stats_service, StatsService
__all__ = ["stats_service", "StatsService"]
RIGHT (actual code):
# app/modules/analytics/services/stats_service.py
class StatsService:
def get_stats(self, db: Session, vendor_id: int):
# Actual implementation here
...
pattern:
file_pattern: "app/modules/*/services/*.py"
anti_patterns:
- "^from app\\.services\\."
- "^from app\\/services\\/"
- id: "MOD-003"
name: "Module schemas must contain actual code, not re-exports"
severity: "warning"
description: |
Self-contained module schemas should contain actual Pydantic models,
not just re-exports from legacy models/schema/ location.
WRONG (re-export only):
# app/modules/analytics/schemas/stats.py
from models.schema.stats import StatsResponse, ...
__all__ = ["StatsResponse", ...]
RIGHT (actual code):
# app/modules/analytics/schemas/stats.py
class StatsResponse(BaseModel):
total: int
pending: int
...
pattern:
file_pattern: "app/modules/*/schemas/*.py"
anti_patterns:
- "^from models\\.schema\\."
- "^from models\\/schema\\/"
- id: "MOD-004"
name: "Module routes must use module-internal implementations"
severity: "warning"
description: |
Module routes should import services from within the module,
not from legacy app/services/ locations.
WRONG:
from app.services.stats_service import stats_service
RIGHT:
from app.modules.analytics.services import stats_service
# or
from ..services import stats_service
pattern:
file_pattern: "app/modules/*/routes/**/*.py"
anti_patterns:
- "from app\\.services\\."
- id: "MOD-005"
name: "Modules with UI must have templates and static directories"
severity: "warning"
description: |
Modules that define menu_items (have UI pages) should have:
- templates/{module_code}/admin/ or templates/{module_code}/vendor/
- static/admin/js/ or static/vendor/js/
This ensures:
- UI is self-contained within the module
- Templates are namespaced to avoid conflicts
- JavaScript can be module-specific
pattern:
directory_pattern: "app/modules/*/"
required_when: "has_menu_items=True"
required_dirs:
- "templates/"
- "static/"
- id: "MOD-006"
name: "Module locales should exist for internationalization"
severity: "info"
description: |
Self-contained modules should have a locales/ directory with
translation files for internationalization.
Structure:
app/modules/{module}/locales/
en.json
de.json
fr.json
lu.json
Translation keys are namespaced as {module}.key_name
pattern:
directory_pattern: "app/modules/*/"
suggested_dirs:
- "locales/"
- id: "MOD-008"
name: "Self-contained modules must have exceptions.py"
severity: "warning"
description: |
Self-contained modules should have an exceptions.py file defining
module-specific exceptions that inherit from WizamartException.
Structure:
app/modules/{module}/exceptions.py
Example:
# app/modules/analytics/exceptions.py
from app.exceptions import WizamartException
class AnalyticsException(WizamartException):
"""Base exception for analytics module."""
pass
class ReportGenerationError(AnalyticsException):
"""Error generating analytics report."""
pass
WHY THIS MATTERS:
- Encapsulation: Module owns its exception hierarchy
- Clarity: Clear which exceptions belong to which module
- Testability: Easy to mock module-specific exceptions
pattern:
directory_pattern: "app/modules/*/"
required_when: "is_self_contained=True"
required_files:
- "exceptions.py"
# =========================================================================
# Auto-Discovery Rules
# =========================================================================
- id: "MOD-009"
name: "Module must have definition.py for auto-discovery"
severity: "error"
description: |
Every module directory must have a definition.py file containing
a ModuleDefinition instance for auto-discovery.
The framework auto-discovers modules from app/modules/*/definition.py.
Without this file, the module won't be registered.
Required:
app/modules/<code>/definition.py
The definition.py must export a ModuleDefinition instance:
# app/modules/analytics/definition.py
from app.modules.base import ModuleDefinition
analytics_module = ModuleDefinition(
code="analytics",
name="Analytics & Reporting",
...
)
pattern:
directory_pattern: "app/modules/*/"
required_files:
- "definition.py"
- "__init__.py"
- id: "MOD-010"
name: "Module routes must export router variable for auto-discovery"
severity: "warning"
description: |
Route files (admin.py, vendor.py, shop.py) in routes/api/ and routes/pages/
must export a 'router' variable for auto-discovery.
The route discovery system looks for:
- routes/api/admin.py with 'router' variable
- routes/api/vendor.py with 'router' variable
- routes/pages/vendor.py with 'router' variable
Example:
# app/modules/analytics/routes/pages/vendor.py
from fastapi import APIRouter
router = APIRouter() # Must be named 'router'
@router.get("/analytics")
def analytics_page():
...
pattern:
file_pattern: "app/modules/*/routes/*/*.py"
required_exports:
- "router"
- id: "MOD-011"
name: "Module tasks must have __init__.py for Celery discovery"
severity: "warning"
description: |
If a module has a tasks/ directory, it must have __init__.py
for Celery task auto-discovery to work.
Celery uses autodiscover_tasks() which requires the tasks
package to be importable.
Required structure:
app/modules/<code>/tasks/
├── __init__.py <- Required for discovery
└── some_task.py
The __init__.py should import task functions:
# app/modules/billing/tasks/__init__.py
from app.modules.billing.tasks.subscription import reset_period_counters
pattern:
directory_pattern: "app/modules/*/tasks/"
required_files:
- "__init__.py"
- id: "MOD-012"
name: "Module locales should have all supported language files"
severity: "info"
description: |
Module locales/ directory should have translation files for
all supported languages to ensure consistent i18n.
Supported languages: en, de, fr, lu
Structure:
app/modules/<code>/locales/
├── en.json
├── de.json
├── fr.json
└── lu.json
Missing translations will fall back to English, but it's
better to have all languages covered.
pattern:
directory_pattern: "app/modules/*/locales/"
suggested_files:
- "en.json"
- "de.json"
- "fr.json"
- "lu.json"
- id: "MOD-007"
name: "Module definition must match directory structure"
severity: "error"
description: |
If a module definition specifies paths like services_path, models_path,
etc., those directories must exist and contain __init__.py files.
Example: If definition.py has:
services_path="app.modules.analytics.services"
Then these must exist:
app/modules/analytics/services/
app/modules/analytics/services/__init__.py
pattern:
file_pattern: "app/modules/*/definition.py"
validates:
- "services_path -> services/__init__.py"
- "models_path -> models/__init__.py"
- "schemas_path -> schemas/__init__.py"
- "exceptions_path -> exceptions.py or exceptions/__init__.py"
- "templates_path -> templates/"
- "locales_path -> locales/"
# =========================================================================
# Self-Contained Config & Migrations Rules
# =========================================================================
- id: "MOD-013"
name: "Module config.py for environment-based configuration"
severity: "info"
description: |
Self-contained modules can have a config.py file for environment-based
configuration using Pydantic Settings.
Structure:
app/modules/<code>/config.py
Pattern:
# app/modules/marketplace/config.py
from pydantic import Field
from pydantic_settings import BaseSettings
class MarketplaceConfig(BaseSettings):
'''Configuration for marketplace module.'''
# Settings prefixed with MARKETPLACE_ in environment
api_timeout: int = Field(default=30, description="API timeout")
batch_size: int = Field(default=100, description="Import batch size")
model_config = {"env_prefix": "MARKETPLACE_"}
# Export for auto-discovery
config_class = MarketplaceConfig
config = MarketplaceConfig()
The config is auto-discovered by app/modules/config.py and can be
accessed via get_module_config("marketplace").
WHY THIS MATTERS:
- Encapsulation: Module owns its configuration
- Environment: Settings loaded from environment variables
- Validation: Pydantic validates configuration on startup
- Defaults: Sensible defaults in code, overridable via env
pattern:
directory_pattern: "app/modules/*/"
suggested_files:
- "config.py"
- id: "MOD-014"
name: "Module migrations must follow naming convention"
severity: "warning"
description: |
If a module has migrations, they must:
1. Be located in migrations/versions/ directory
2. Follow the naming convention: {module_code}_{sequence}_{description}.py
Structure:
app/modules/<code>/migrations/
├── __init__.py
└── versions/
├── __init__.py
├── <code>_001_initial.py
├── <code>_002_add_feature.py
└── ...
Example for cms module:
app/modules/cms/migrations/versions/
├── cms_001_create_content_pages.py
├── cms_002_add_sections.py
└── cms_003_add_media_library.py
Migration file must include:
revision = "cms_001"
down_revision = None # or previous revision
branch_labels = ("cms",) # Module-specific branch
WHY THIS MATTERS:
- Isolation: Module migrations don't conflict with core
- Ordering: Sequence numbers ensure correct order
- Traceability: Clear which module owns each migration
- Rollback: Can rollback module migrations independently
pattern:
directory_pattern: "app/modules/*/migrations/versions/"
file_pattern: "{module_code}_*.py"
- id: "MOD-015"
name: "Module migrations directory must have __init__.py files"
severity: "warning"
description: |
If a module has a migrations/ directory, both the migrations/
and migrations/versions/ directories must have __init__.py files
for proper Python package discovery.
Required structure:
app/modules/<code>/migrations/
├── __init__.py <- Required
└── versions/
└── __init__.py <- Required
The __init__.py files can be empty or contain docstrings:
# migrations/__init__.py
'''Module migrations.'''
WHY THIS MATTERS:
- Alembic needs to import migration scripts as Python modules
- Without __init__.py, the directories are not Python packages
pattern:
directory_pattern: "app/modules/*/migrations/"
required_files:
- "__init__.py"
- "versions/__init__.py"
# =========================================================================
# Legacy Location Rules (Auto-Discovery Enforcement)
# =========================================================================
- id: "MOD-016"
name: "Routes must be in modules, not app/api/v1/"
severity: "error"
description: |
All API routes must be defined in module directories, not in legacy
app/api/v1/vendor/ or app/api/v1/admin/ locations.
WRONG (legacy location):
app/api/v1/vendor/orders.py
app/api/v1/admin/orders.py
RIGHT (module location):
app/modules/orders/routes/api/vendor.py
app/modules/orders/routes/api/admin.py
Routes in modules are auto-discovered and registered. Legacy routes
require manual registration and don't follow module patterns.
EXCEPTIONS (allowed in legacy):
- __init__.py (router aggregation)
- auth.py (core authentication - will move to tenancy)
- Files with # noqa: mod-016 comment
WHY THIS MATTERS:
- Auto-discovery: Module routes are automatically registered
- Encapsulation: Routes belong with their domain logic
- Consistency: All modules follow the same pattern
- Maintainability: Easier to understand module boundaries
pattern:
prohibited_locations:
- "app/api/v1/vendor/*.py"
- "app/api/v1/admin/*.py"
exceptions:
- "__init__.py"
- "auth.py"
- id: "MOD-017"
name: "Services must be in modules, not app/services/"
severity: "error"
description: |
All business logic services must be defined in module directories,
not in the legacy app/services/ location.
WRONG (legacy location):
app/services/order_service.py
RIGHT (module location):
app/modules/orders/services/order_service.py
EXCEPTIONS (allowed in legacy):
- __init__.py (re-exports for backwards compatibility)
- Files that are pure re-exports from modules
- Files with # noqa: mod-017 comment
WHY THIS MATTERS:
- Encapsulation: Services belong with their domain
- Clear boundaries: Know which module owns which service
- Testability: Can test modules in isolation
- Refactoring: Easier to move/rename modules
pattern:
prohibited_locations:
- "app/services/*.py"
exceptions:
- "__init__.py"
- id: "MOD-018"
name: "Tasks must be in modules, not app/tasks/"
severity: "error"
description: |
All Celery background tasks must be defined in module directories,
not in the legacy app/tasks/ location.
WRONG (legacy location):
app/tasks/subscription_tasks.py
RIGHT (module location):
app/modules/billing/tasks/subscription.py
The module tasks/ directory must have __init__.py for Celery
autodiscovery to work.
EXCEPTIONS (allowed in legacy):
- __init__.py (Celery app configuration)
- dispatcher.py (task routing infrastructure)
- Files with # noqa: mod-018 comment
WHY THIS MATTERS:
- Auto-discovery: Celery finds tasks from module directories
- Encapsulation: Tasks belong with their domain logic
- Consistency: All async operations in one place per module
pattern:
prohibited_locations:
- "app/tasks/*.py"
exceptions:
- "__init__.py"
- "dispatcher.py"
- id: "MOD-019"
name: "Schemas must be in modules, not models/schema/"
severity: "error"
description: |
All Pydantic schemas must be defined in module directories,
not in the legacy models/schema/ location.
WRONG (legacy location):
models/schema/order.py
RIGHT (module location):
app/modules/orders/schemas/order.py
EXCEPTIONS (allowed in legacy):
- __init__.py (re-exports for backwards compatibility)
- base.py (base schema classes - infrastructure)
- auth.py (core authentication schemas - cross-cutting)
- Files with # noqa: mod-019 comment
WHY THIS MATTERS:
- Encapsulation: Schemas belong with their domain
- Co-location: Request/response schemas near route handlers
- Clear ownership: Know which module owns which schema
pattern:
prohibited_locations:
- "models/schema/*.py"
exceptions:
- "__init__.py"
- "base.py"
- "auth.py"
# =========================================================================
# Module Definition Completeness Rules
# =========================================================================
- id: "MOD-020"
name: "Module definition must have required attributes"
severity: "warning"
description: |
Module definitions should include at minimum:
- code: Module identifier
- name: Human-readable name
- description: What the module does
- version: Semantic version
- features: List of features (unless infrastructure module)
- permissions: Access control definitions (unless internal or storefront-only)
EXAMPLES (incomplete):
module = ModuleDefinition(
code="cart",
name="Shopping Cart",
description="...",
version="1.0.0",
# Missing features and permissions
)
EXAMPLES (complete):
module = ModuleDefinition(
code="orders",
name="Order Management",
description="...",
version="1.0.0",
features=["order_management", "fulfillment_tracking"],
permissions=[
PermissionDefinition(id="orders.view", ...),
],
)
EXCEPTIONS:
- is_internal=True modules may skip permissions
- Infrastructure modules (is_core=True with no UI) may skip features
- Storefront-only modules (session-based, no admin UI) may have minimal permissions
WHY THIS MATTERS:
- Consistency: All modules follow the same definition pattern
- RBAC: Permissions enable proper role-based access control
- Feature flags: Features enable selective module functionality
pattern:
file_pattern: "app/modules/*/definition.py"
required_attributes:
- "code"
- "name"
- "description"
- "version"
- id: "MOD-021"
name: "Modules with menus should have features"
severity: "warning"
description: |
If a module defines menu items or menu sections, it should also
define features to describe what functionality it provides.
Menus indicate the module has UI and user-facing functionality,
which should be documented as features.
WRONG:
module = ModuleDefinition(
code="billing",
menus={
FrontendType.ADMIN: [...],
},
# Missing features list
)
RIGHT:
module = ModuleDefinition(
code="billing",
features=[
"subscription_management",
"billing_history",
"invoice_generation",
],
menus={
FrontendType.ADMIN: [...],
},
)
WHY THIS MATTERS:
- Documentation: Features describe what the module does
- Feature flags: Enables/disables specific functionality
- Consistency: All UI modules describe their capabilities
pattern:
file_pattern: "app/modules/*/definition.py"
validates:
- "menus -> features"
- id: "MOD-022"
name: "Feature modules should have permissions"
severity: "info"
description: |
Modules with features should define permissions unless:
- is_internal=True (internal tools like dev_tools)
- Storefront-only module (session-based, no authentication)
Permissions enable role-based access control (RBAC) for module
functionality.
EXCEPTIONS:
- is_internal=True modules (internal tooling)
- Modules with only storefront features (cart, checkout without admin UI)
- Infrastructure modules (contracts, core utilities)
EXAMPLE:
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",
),
],
)
WHY THIS MATTERS:
- RBAC: Permissions enable proper access control
- Security: Restrict who can access module features
- Consistency: All feature modules define their access rules
pattern:
file_pattern: "app/modules/*/definition.py"
validates:
- "features -> permissions"
exceptions:
- "is_internal=True"
- id: "MOD-023"
name: "Modules with routers should use get_*_with_routers pattern"
severity: "info"
description: |
Modules that define routers (admin_router, vendor_router, etc.)
should follow the lazy import pattern with a dedicated function:
def get_{module}_module_with_routers() -> ModuleDefinition:
This pattern:
1. Avoids circular imports during module initialization
2. Ensures routers are attached at the right time
3. Provides a consistent API for router registration
WRONG:
# Direct router assignment at module level
module.admin_router = admin_router
RIGHT:
def _get_admin_router():
from app.modules.orders.routes.admin import admin_router
return admin_router
def get_orders_module_with_routers() -> ModuleDefinition:
orders_module.admin_router = _get_admin_router()
return orders_module
WHY THIS MATTERS:
- Prevents circular imports
- Consistent pattern across all modules
- Clear API for module registration
pattern:
file_pattern: "app/modules/*/definition.py"
validates:
- "router imports -> get_*_with_routers function"
# =========================================================================
# Static File Mounting Rules
# =========================================================================
- id: "MOD-024"
name: "Module static file mount order - specific paths first"
severity: "error"
description: |
When mounting module static files in main.py, more specific paths must
be mounted BEFORE less specific paths. FastAPI processes mounts in
registration order.
WRONG ORDER (locales 404):
# Less specific first - intercepts /static/modules/X/locales/*
app.mount("/static/modules/X", StaticFiles(...))
# More specific second - never reached!
app.mount("/static/modules/X/locales", StaticFiles(...))
RIGHT ORDER (locales work):
# More specific first
app.mount("/static/modules/X/locales", StaticFiles(...))
# Less specific second - catches everything else
app.mount("/static/modules/X", StaticFiles(...))
This applies to all nested static file mounts:
- locales/ must be mounted before static/
- img/ or css/ subdirectories must be mounted before parent
SYMPTOMS OF WRONG ORDER:
- 404 errors for nested paths like /static/modules/tenancy/locales/en.json
- Requests to subdirectories served as 404 instead of finding files
See: docs/architecture/user-context-pattern.md (Static File Mount Order section)
pattern:
file_pattern: "main.py"
validates:
- "module_locales mount BEFORE module_static mount"