# 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//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//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//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//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//migrations/ ├── __init__.py └── versions/ ├── __init__.py ├── _001_initial.py ├── _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//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"