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>
764 lines
26 KiB
YAML
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"
|