# 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 #} ... ``` **Recognized markers:** - `{# standalone #}` - Jinja comment style - `{# noqa: TPL-001 #}` - Standard noqa style - `` - 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 ✅

{{ item.name }}

``` #### 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 ✅
``` #### TPL-006: Implement Loading State **Severity:** Warning All templates that load data should show loading state. ```html ✅
Loading...
``` #### TPL-007: Implement Empty State **Severity:** Warning Show empty state when lists have no items. ```html ✅ ``` #### 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 %} {% endblock %} {# ❌ Bad - Invalid block name (silently ignored!) #} {% block page_scripts %} {% 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']) }} ... {% endcall %} {# ❌ Bad - Copy-pasting table HTML #}
......
``` #### 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 `` 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 #} ``` **Suppress with `noqa` for ID fields:** ```jinja {# noqa: FE-008 - User ID is typed directly, not incremented #} ``` --- ### 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 ✅ ``` #### 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