Files
orion/.architecture-rules/module.yaml
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +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 OrionException.
Structure:
app/modules/{module}/exceptions.py
Example:
# app/modules/analytics/exceptions.py
from app.exceptions import OrionException
class AnalyticsException(OrionException):
"""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"