{"id": "doctype-mockdoca", "type": "doctype", "title": "DocType: MockDocA", "content": "# MockDocA\n\n**Source**: [verify_selective_migrations.py](file:///builds/castlecraft/framework-m/scripts/verify_selective_migrations.py)\n\n## Fields\n\n_No fields defined._\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "MockDocA"}} {"id": "doctype-todo", "type": "doctype", "title": "DocType: Todo", "content": "# Todo\n\nTodo DocType for testing.\n\n**Source**: [run_server.py](file:///builds/castlecraft/framework-m/libs/framework-m/examples/run_server.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| title | str | | Todo title | - |\n| completed | bool | | - | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| Employee | \u2713 | | \u2713 | \u2713 |\n| Guest | | | \u2713 | |\n| Manager | \u2713 | | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Todo"}} {"id": "doctype-scheduledjob", "type": "doctype", "title": "DocType: ScheduledJob", "content": "# ScheduledJob\n\nPersistent scheduled job configuration.\n\n Stores cron job definitions that are loaded by the worker\n process at startup, enabling dynamic job scheduling.\n\n Attributes:\n name: Unique job identifier\n function: Dotted path to job function (e.g., 'myapp.jobs.send_email')\n cron_expression: Cron syntax string (e.g., '0 9 * * *')\n enabled: Whether the job is active\n last_run: Timestamp of last execution (updated by worker)\n next_run: Timestamp of next scheduled run (calculated)\n\n**Source**: [scheduled_job.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/scheduled_job.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| name | str | \u2713 | Unique job identifier | minLen: 1, maxLen: 255 |\n| function | str | \u2713 | Dotted path to job function (e.g., 'myapp.jobs.send_email') | minLen: 1 |\n| cron_expression | str | \u2713 | Cron syntax (e.g., '0 9 * * *' for daily at 9 AM) | minLen: 1 |\n| enabled | bool | | Whether the job is active and should be scheduled | - |\n| last_run | datetime | None | | Timestamp of last execution (set by worker) | - |\n| next_run | datetime | None | | Timestamp of next scheduled run (calculated) | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| Employee | | | \u2713 | |\n| Manager | \u2713 | | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "ScheduledJob"}} {"id": "doctype-todo", "type": "doctype", "title": "DocType: Todo", "content": "# Todo\n\nA simple todo item.\n\n Attributes:\n title: The title/name of the todo item\n description: Optional description of the todo\n completed: Whether the todo is completed\n priority: Priority level (Low, Medium, High)\n\n**Source**: [todo.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/todo.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| title | str | | Title of the todo item | minLen: 1, maxLen: 200 |\n| description | str | None | | Optional description | maxLen: 1000 |\n| completed | bool | | Whether the todo is completed | - |\n| priority | str | | Priority level | pattern: `^(Low|Medium|High)$` |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Todo"}} {"id": "doctype-translation", "type": "doctype", "title": "DocType: Translation", "content": "# Translation\n\nTranslation for multilingual text.\n\n Attributes:\n source_text: Original text in default language (usually English)\n translated_text: Translated text in target locale\n locale: Target locale code (e.g., \"fr\", \"es\", \"hi\", \"ta\")\n context: Optional context for disambiguation (e.g., \"button\", \"label\", \"error\")\n\n Constraints:\n Unique: (source_text, locale, context) - prevents duplicate translations\n\n Example:\n # Simple translation\n t1 = Translation(\n source_text=\"Save\",\n translated_text=\"Enregistrer\",\n locale=\"fr\",\n )\n\n # Context-aware translation\n t2 = Translation(\n source_text=\"Save\",\n translated_text=\"Sauvegarder\",\n locale=\"fr\",\n context=\"button\",\n )\n\n**Source**: [translation.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/translation.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| source_text | str | | Original text in default language | minLen: 1, maxLen: 1000 |\n| translated_text | str | | Translated text in target locale | minLen: 1, maxLen: 1000 |\n| locale | str | | Target locale code (e.g., 'fr', 'es', 'hi') | minLen: 2, maxLen: 10, pattern: `^[a-z]{2}(_[A-Z]{2})?$` |\n| context | str | None | | Optional context for disambiguation | maxLen: 100 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Translation"}} {"id": "doctype-errorlog", "type": "doctype", "title": "DocType: ErrorLog", "content": "# ErrorLog\n\nError log entry DocType.\n\n Stores information about system errors for debugging.\n Errors are captured by the global exception handler.\n\n Attributes:\n title: Short description of the error\n error_type: Exception class name (e.g., \"ValueError\")\n error_message: Full error message\n traceback: Stack trace for debugging\n request_url: URL that triggered the error\n user_id: User who triggered the error\n request_id: Request ID for correlation\n timestamp: When the error occurred\n context: Additional context (headers, params, etc.)\n\n Example:\n log = ErrorLog(\n title=\"Validation failed\",\n error_type=\"ValidationError\",\n error_message=\"Field 'email' is invalid\",\n )\n\n**Source**: [error_log.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/error_log.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| title | str | \u2713 | Short description of the error | maxLen: 255 |\n| error_type | str | \u2713 | Exception class name (e.g., ValueError) | - |\n| error_message | str | \u2713 | Full error message | - |\n| traceback | str | None | | Full stack trace for debugging | - |\n| request_url | str | None | | URL that triggered the error | - |\n| user_id | str | None | | User who triggered the error | - |\n| request_id | str | None | | Request ID for log correlation | - |\n| timestamp | datetime | | When the error occurred (UTC) | - |\n| context | dict[str, Any] | None | | Additional context: headers, params, etc. | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| System Manager | \u2713 | | \u2713 | |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "ErrorLog"}} {"id": "doctype-workflowstate", "type": "doctype", "title": "DocType: WorkflowState", "content": "# WorkflowState\n\nTracks the current workflow state of a document.\n\n Attributes:\n workflow: Name of the workflow\n doctype: The DocType of the document in workflow\n document_name: The name/ID of the document\n current_state: Current state name in the workflow\n updated_at: When the state was last updated\n\n**Source**: [workflow_state.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/workflow_state.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| workflow | str | | Workflow name | - |\n| doctype | str | | Target DocType | - |\n| document_name | str | | Document ID | - |\n| current_state | str | | Current workflow state | - |\n| updated_at | datetime | | Last state update timestamp | - |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "WorkflowState"}} {"id": "doctype-tenanttranslation", "type": "doctype", "title": "DocType: TenantTranslation", "content": "# TenantTranslation\n\nTenant-specific translation overrides.\n\n Allows tenants to customize translations to match their terminology,\n industry, or brand voice. Overrides system translations when present.\n\n Attributes:\n tenant_id: Tenant identifier (from TenantContext)\n source_text: Original text to translate (1-1000 characters)\n translated_text: Translated text in target locale (1-1000 characters)\n locale: Locale code (e.g., \"en\", \"hi\", \"ta\", \"es_MX\")\n context: Optional context for disambiguation (e.g., \"button\", \"field_label\")\n\n Constraints:\n - Unique combination of (tenant_id, source_text, locale, context)\n - locale must match pattern: 2 lowercase letters, optionally followed by\n underscore and 2 uppercase letters (e.g., \"en\", \"en_US\", \"pt_BR\")\n\n Example:\n # Healthcare tenant uses \"Patient\" instead of \"Customer\"\n TenantTranslation(\n tenant_id=\"healthcare-corp\",\n source_text=\"Customer\",\n translated_text=\"Patient\",\n locale=\"en\",\n context=\"field_label\"\n )\n\n # Retail tenant uses \"Client\" in French\n TenantTranslation(\n tenant_id=\"retail-corp\",\n source_text=\"Customer\",\n translated_text=\"Client\",\n locale=\"fr\",\n context=\"field_label\"\n )\n\n**Source**: [tenant_translation.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/tenant_translation.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| tenant_id | str | | Tenant identifier from TenantContext | minLen: 1, maxLen: 100 |\n| source_text | str | | Original text to translate | minLen: 1, maxLen: 1000 |\n| translated_text | str | | Translated text in target locale | minLen: 1, maxLen: 1000 |\n| locale | str | | Locale code (e.g., 'en', 'hi', 'ta', 'es_MX') | minLen: 2, maxLen: 10, pattern: `^[a-z]{2}(_[A-Z]{2})?$` |\n| context | str | None | | Optional context for disambiguation (e.g., 'button', 'field_label') | maxLen: 100 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "TenantTranslation"}} {"id": "doctype-notification", "type": "doctype", "title": "DocType: Notification", "content": "# Notification\n\nNotification DocType.\n\n Stores in-app notifications for users.\n\n Attributes:\n user_id: Recipient user ID\n subject: Notification subject/title\n message: Full notification message\n notification_type: Type of notification (info, success, etc.)\n read: Whether the notification has been read\n doctype: Related DocType (optional)\n document_id: Related document ID (optional)\n timestamp: When the notification was created\n from_user: User who triggered the notification (optional)\n metadata: Additional notification data\n\n Example:\n notification = Notification(\n user_id=\"user-001\",\n subject=\"New Comment\",\n message=\"John commented on your invoice.\",\n )\n\n**Source**: [notification.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/notification.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| user_id | str | \u2713 | Recipient user ID | - |\n| subject | str | \u2713 | Notification subject/title | maxLen: 255 |\n| message | str | \u2713 | Full notification message | - |\n| notification_type | str | | Notification type (info, success, warning, error, etc.) | - |\n| read | bool | | Whether the notification has been read | - |\n| doctype | str | None | | Related DocType (e.g., Invoice) | - |\n| document_id | str | None | | Related document ID | - |\n| timestamp | datetime | | When the notification was created (UTC) | - |\n| from_user | str | None | | User who triggered the notification | - |\n| metadata | dict[str, Any] | None | | Additional notification data | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| All | | \u2713 | \u2713 | \u2713 |\n| System Manager | \u2713 | | | |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Notification"}} {"id": "doctype-localuser", "type": "doctype", "title": "DocType: LocalUser", "content": "# LocalUser\n\nSQL-backed user for Indie mode (Mode A).\n\n Stores credentials and profile information locally.\n Default implementation for rapid development.\n\n Attributes:\n email: Unique email address (login identifier)\n password_hash: Argon2 hash of password (excluded from serialization)\n full_name: Optional display name\n is_active: Whether user can log in (default True)\n\n Security:\n - password_hash is excluded from model_dump() and model_dump_json()\n - Never store plaintext passwords\n - Use argon2 for hashing (see LocalIdentityAdapter)\n\n Example:\n user = LocalUser(\n email=\"john@example.com\",\n password_hash=\"$argon2id$v=19$...\",\n full_name=\"John Doe\",\n )\n\n**Source**: [user.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/user.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| email | str | | Unique email address | - |\n| password_hash | str | | Argon2 password hash | - |\n| full_name | str | None | | User's display name | - |\n| is_active | bool | | Whether user can log in | - |\n| locale | str | None | | User's preferred locale (e.g., 'en', 'hi', 'ta') | maxLen: 10 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "LocalUser"}} {"id": "doctype-apikey", "type": "doctype", "title": "DocType: ApiKey", "content": "# ApiKey\n\nAPI key for scripts and integrations.\n\n Attributes:\n key_hash: Argon2 hash of the API key (excluded from serialization)\n user_id: Owner of the key\n name: Human-readable label for the key\n scopes: Optional permission scopes (e.g., [\"read\", \"write\"])\n expires_at: Optional expiration date\n last_used_at: Last time the key was used (for auditing)\n is_active: Whether the key is currently active\n\n Security:\n - key_hash is excluded from model_dump() and model_dump_json()\n - Never store or return plaintext keys\n - Raw key is only shown once at creation time\n\n Example:\n key = ApiKey(\n key_hash=\"$argon2id$v=19$...\",\n user_id=\"user-123\",\n name=\"Production Deployment\",\n scopes=[\"deploy\"],\n expires_at=datetime(2025, 12, 31),\n )\n\n**Source**: [api_key.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/api_key.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| key_hash | str | | Argon2 hash of the API key | - |\n| user_id | str | | Owner user ID | - |\n| name | str | | Human-readable key label | - |\n| scopes | list[str] | | Permission scopes for this key | - |\n| expires_at | datetime | None | | Key expiration date (None = never expires) | - |\n| last_used_at | datetime | None | | Last time the key was used | - |\n| is_active | bool | | Whether the key is active | - |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "ApiKey"}} {"id": "doctype-recentdocument", "type": "doctype", "title": "DocType: RecentDocument", "content": "# RecentDocument\n\nRecentDocument DocType.\n\n Tracks recently viewed documents per user for quick navigation.\n\n Attributes:\n user_id: User who viewed the document\n doctype: DocType of the viewed document\n document_id: ID of the viewed document\n document_name: Display name of the document\n route: URL route to the document\n viewed_at: When the document was last viewed\n\n**Source**: [recent_document.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/recent_document.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| user_id | str | \u2713 | User who viewed the document | - |\n| doctype | str | \u2713 | DocType of the viewed document | - |\n| document_id | str | \u2713 | ID of the viewed document | - |\n| document_name | str | \u2713 | Display name of the document | - |\n| route | str | None | | URL route to the document | - |\n| viewed_at | datetime | | When the document was last viewed | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| All | \u2713 | \u2713 | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "RecentDocument"}} {"id": "doctype-systemsettings", "type": "doctype", "title": "DocType: SystemSettings", "content": "# SystemSettings\n\nSystem settings singleton DocType.\n\n Stores global application configuration. Only one instance exists.\n\n Attributes:\n name: Always \"System Settings\"\n app_name: Application display name\n timezone: Default timezone (e.g., \"UTC\", \"America/New_York\")\n date_format: Date format string (e.g., \"YYYY-MM-DD\")\n time_format: Time format string (e.g., \"HH:mm:ss\")\n language: Default language code (e.g., \"en\")\n enable_signup: Allow new user registration\n session_expiry: Session timeout in minutes\n maintenance_mode: If True, only admins can access\n\n Example:\n settings = SystemSettings(\n app_name=\"My App\",\n timezone=\"America/New_York\",\n )\n\n**Source**: [system_settings.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/system_settings.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| name | str | | Always 'System Settings | - |\n| app_name | str | | Application display name | maxLen: 255 |\n| timezone | str | | Default timezone (e.g., UTC, America/New_York) | - |\n| date_format | str | | Date format (e.g., YYYY-MM-DD, DD/MM/YYYY) | - |\n| time_format | str | | Time format (e.g., HH:mm:ss, hh:mm A) | - |\n| language | str | | Default language code (e.g., en, es, fr) | minLen: 2, maxLen: 10 |\n| enable_signup | bool | | Allow new user registration | - |\n| session_expiry | int | | Session timeout in minutes | min: 1, max: 525600 |\n| maintenance_mode | bool | | If True, only admins can access the application | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| All | | | \u2713 | |\n| System Manager | \u2713 | | | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "SystemSettings"}} {"id": "doctype-documentshare", "type": "doctype", "title": "DocType: DocumentShare", "content": "# DocumentShare\n\nExplicit share of a document with a user or role.\n\n Enables fine-grained document-level sharing beyond the default\n owner-based or team-based RLS.\n\n Attributes:\n doctype_name: The DocType being shared (e.g., \"Invoice\")\n doc_id: Document identifier (e.g., \"INV-001\" or UUID)\n shared_with: User ID or Role name to share with\n share_type: Whether sharing with USER or ROLE\n granted_permissions: List of permissions granted (e.g., [\"read\", \"write\"])\n note: Optional note explaining why the share was created\n\n**Source**: [document_share.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/document_share.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| doctype_name | str | | DocType name of the shared document | - |\n| doc_id | str | | ID or name of the document being shared | - |\n| shared_with | str | | User ID or Role name to share with | - |\n| share_type | ShareType | | Whether sharing with a user or role | - |\n| granted_permissions | list[str] | | Permissions granted: read, write, delete, etc. | - |\n| note | str | None | | Reason for sharing (for audit) | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| Employee | \u2713 | | \u2713 | \u2713 |\n| Manager | \u2713 | | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "DocumentShare"}} {"id": "doctype-emailqueue", "type": "doctype", "title": "DocType: EmailQueue", "content": "# EmailQueue\n\nEmail queue DocType.\n\n Stores outbound emails for asynchronous processing.\n\n Attributes:\n to: List of recipient email addresses\n cc: Carbon copy recipients\n bcc: Blind carbon copy recipients\n subject: Email subject line\n body: Email body (HTML)\n text_body: Plain text alternative\n status: Queue status (Queued, Sending, Sent, Failed)\n priority: Email priority (low, normal, high)\n error: Error message if failed\n retry_count: Number of retry attempts\n max_retries: Maximum retry attempts\n queued_at: When the email was queued\n sent_at: When the email was sent\n from_address: Sender email address\n reply_to: Reply-to address\n attachments: Attachment metadata\n reference_doctype: Related DocType (optional)\n reference_id: Related document ID (optional)\n\n Example:\n email = EmailQueue(\n to=[\"user@example.com\"],\n subject=\"Invoice\",\n body=\"

Your invoice is attached.

\",\n )\n\n**Source**: [email_queue.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/email_queue.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| to | list[str] | \u2713 | Recipient email addresses | minLen: 1 |\n| subject | str | \u2713 | Email subject line | maxLen: 500 |\n| body | str | \u2713 | Email body (HTML) | - |\n| cc | list[str] | None | | Carbon copy recipients | - |\n| bcc | list[str] | None | | Blind carbon copy recipients | - |\n| text_body | str | None | | Plain text alternative | - |\n| status | str | | Queue status (Queued, Sending, Sent, Failed) | - |\n| priority | str | | Email priority (low, normal, high) | - |\n| error | str | None | | Error message if failed | - |\n| retry_count | int | | Number of retry attempts | min: 0 |\n| max_retries | int | | Maximum retry attempts | min: 0, max: 10 |\n| queued_at | datetime | | When the email was queued (UTC) | - |\n| sent_at | datetime | None | | When the email was sent (UTC) | - |\n| from_address | str | None | | Sender email address (uses default if not specified) | - |\n| reply_to | str | None | | Reply-to address | - |\n| attachments | list[dict[str, Any]] | None | | Attachment metadata (file_id, filename, content_type) | - |\n| reference_doctype | str | None | | Related DocType (e.g., Invoice) | - |\n| reference_id | str | None | | Related document ID | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| System Manager | \u2713 | \u2713 | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "EmailQueue"}} {"id": "doctype-webhook", "type": "doctype", "title": "DocType: Webhook", "content": "# Webhook\n\nOutgoing webhook configuration.\n\n Defines webhooks that listen to events and deliver HTTP\n notifications to external endpoints with optional filtering.\n\n Attributes:\n name: Unique webhook identifier\n event: Event to listen to (e.g., \"doc.created\", \"doc.updated\")\n doctype_filter: Optional DocType filter (e.g., \"Invoice\")\n condition: Optional JMESPath/SimpleEval expression to filter events\n url: Webhook endpoint URL\n method: HTTP method (POST or PUT)\n headers: Custom HTTP headers to send\n payload_transform: Optional Jinja2 template to transform event payload\n enabled: Whether the webhook is active\n secret: Secret for HMAC-SHA256 signature verification\n\n**Source**: [webhook.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/webhook.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| name | str | \u2713 | Unique webhook identifier | minLen: 1, maxLen: 255 |\n| event | str | \u2713 | Event to listen to (e.g., 'doc.created') | minLen: 1 |\n| doctype_filter | str | None | | Filter by DocType (e.g., 'Invoice', 'Order') | - |\n| condition | str | None | | JMESPath or SimpleEval expression to filter events (e.g., 'status == `paid`') | - |\n| url | str | \u2713 | Webhook endpoint URL | minLen: 1 |\n| method | str | | HTTP method (POST or PUT) | - |\n| headers | dict[str, Any] | | Custom HTTP headers to send with request | - |\n| payload_transform | str | None | | Jinja2 template to transform event payload (e.g., \\'{\"text\": \"{{ doc.name }}\"}\\') | - |\n| enabled | bool | | Whether the webhook is active | - |\n| secret | str | None | | Secret for HMAC-SHA256 signature (X-Webhook-Signature header) | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| Manager | | | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Webhook"}} {"id": "doctype-custompermission", "type": "doctype", "title": "DocType: CustomPermission", "content": "# CustomPermission\n\nDatabase-stored permission override.\n\n Allows admins to override code-first permissions with database rules.\n Evaluated in the RbacPermissionAdapter alongside Meta.permissions.\n\n Evaluation Priority:\n 1. Explicit DENY rules are checked first (deny wins)\n 2. If no deny, explicit ALLOW rules are checked\n 3. If no explicit rule, fall back to code-first Meta.permissions\n\n Attributes:\n for_user: Optional user ID this rule applies to\n for_role: Optional role name this rule applies to\n doctype_name: The DocType this permission applies to (\"*\" for all)\n action: The action (read, write, create, delete, submit, etc.)\n effect: Whether to ALLOW or DENY the permission\n enabled: Whether this rule is active\n priority: Rule priority (higher = evaluated first)\n description: Human-readable description of why this rule exists\n\n**Source**: [custom_permission.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/custom_permission.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| for_user | str | None | | User ID this rule applies to (None = all users) | - |\n| for_role | str | None | | Role name this rule applies to (None = all roles) | - |\n| doctype_name | str | | DocType name or '*' for all DocTypes | - |\n| action | str | | Action: read, write, create, delete, submit, cancel, amend | - |\n| effect | PermissionEffect | | Whether to allow or deny this permission | - |\n| enabled | bool | | Whether this rule is active | - |\n| priority | int | | Rule priority (higher = evaluated first) | - |\n| description | str | None | | Why this rule exists (for audit) | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| System | | | \u2713 | |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "CustomPermission"}} {"id": "doctype-workflow", "type": "doctype", "title": "DocType: Workflow", "content": "# Workflow\n\nDefines a complete workflow configuration.\n\n Attributes:\n doctype: The target DocType this workflow applies to\n initial_state: The starting state for new documents\n states: List of state definitions with metadata\n is_active: Whether this workflow is currently active\n\n**Source**: [workflow.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/workflow.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| doctype | str | | Target DocType for this workflow | - |\n| initial_state | str | | Initial state for new documents | - |\n| states | list[dict[str, str]] | | State definitions with name and optional metadata | - |\n| is_active | bool | | Whether workflow is active | - |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Workflow"}} {"id": "doctype-socialaccount", "type": "doctype", "title": "DocType: SocialAccount", "content": "# SocialAccount\n\nLinks OAuth2/OIDC provider identity to local user.\n\n Attributes:\n provider: OAuth2 provider name (google, github, microsoft, oidc)\n provider_user_id: Unique ID from the provider\n user_id: Local user ID this account belongs to\n display_name: Display name from provider (for UI)\n email: Email from provider (for lookup, optional)\n last_login_at: Last time this social account was used\n\n Security:\n - No sensitive data (tokens, secrets) stored here\n - Only identity linking information\n - Tokens are handled by OAuth2 flow, not persisted\n\n Example:\n # User signs in with Google\n account = SocialAccount(\n provider=\"google\",\n provider_user_id=\"1234567890\",\n user_id=\"user-001\",\n display_name=\"John Doe\",\n )\n\n**Source**: [social_account.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/social_account.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| provider | str | | OAuth2 provider (google, github, microsoft, oidc) | - |\n| provider_user_id | str | | Unique user ID from the provider | - |\n| user_id | str | | Local user ID this account belongs to | - |\n| display_name | str | | Display name from provider | - |\n| email | str | None | | Email from provider (for lookup) | - |\n| last_login_at | datetime | None | | Last time this social account was used for login | - |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "SocialAccount"}} {"id": "doctype-webhooklog", "type": "doctype", "title": "DocType: WebhookLog", "content": "# WebhookLog\n\nWebhook delivery audit log.\n\n Records each webhook delivery attempt with status, response,\n and error information for debugging and auditing.\n\n Attributes:\n webhook: Reference to the Webhook DocType name\n event: The event that triggered the webhook\n status: Delivery status ('success' or 'failed')\n response_code: HTTP response status code\n response_body: HTTP response body (truncated)\n error: Error message if delivery failed\n timestamp: When the delivery was attempted\n\n**Source**: [webhook_log.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/webhook_log.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| webhook | str | \u2713 | Reference to Webhook name | minLen: 1 |\n| event | str | \u2713 | Event that triggered the webhook (e.g., 'doc.created') | minLen: 1 |\n| status | str | \u2713 | Delivery status: 'success' or 'failed | - |\n| response_code | int | \u2713 | HTTP response status code | - |\n| response_body | str | None | | HTTP response body (may be truncated) | - |\n| error | str | None | | Error message if delivery failed | - |\n| timestamp | datetime | | When the delivery was attempted | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| Manager | | | \u2713 | |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "WebhookLog"}} {"id": "doctype-file", "type": "doctype", "title": "DocType: File", "content": "# File\n\nFile attachment DocType.\n\n Stores metadata about uploaded files. Actual file content\n is stored via StorageAdapter (local, S3, GCS, etc.).\n\n Attributes:\n file_name: Original filename as uploaded\n file_url: Storage URL/path for retrieval\n file_size: Size in bytes\n content_type: MIME type (e.g., \"application/pdf\")\n attached_to_doctype: DocType this file is attached to\n attached_to_name: Document ID this file is attached to\n is_private: If True, requires authentication to access\n\n Example:\n file = File(\n file_name=\"report.pdf\",\n file_url=\"/files/2024/01/report-abc123.pdf\",\n file_size=51200,\n content_type=\"application/pdf\",\n is_private=True,\n )\n\n**Source**: [file.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/file.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| file_name | str | | Original filename | - |\n| file_url | str | | Storage URL/path | - |\n| file_size | int | | File size in bytes | - |\n| content_type | str | | MIME type | - |\n| attached_to_doctype | str | None | | DocType this file is attached to | - |\n| attached_to_name | str | None | | Document ID this file is attached to | - |\n| is_private | bool | | If True, requires authentication | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Guest | | | \u2713 | |\n| User | \u2713 | \u2713 | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "File"}} {"id": "doctype-session", "type": "doctype", "title": "DocType: Session", "content": "# Session\n\nDatabase-backed user session.\n\n Attributes:\n session_id: Unique session identifier (used as cookie value)\n user_id: User this session belongs to\n expires_at: When the session expires\n ip_address: Client IP address for security auditing\n user_agent: Client user agent for display\n\n Security:\n - session_id should be cryptographically random\n - Check expires_at before accepting session\n - Track ip_address/user_agent for security alerts\n\n**Source**: [session.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/session.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| session_id | str | | Unique session identifier | - |\n| user_id | str | | User this session belongs to | - |\n| expires_at | datetime | | Session expiration time | - |\n| ip_address | str | None | | Client IP address | - |\n| user_agent | str | None | | Client user agent string | - |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Session"}} {"id": "doctype-joblog", "type": "doctype", "title": "DocType: JobLog", "content": "# JobLog\n\nBackground job execution log.\n\n Records job execution status and results for monitoring,\n debugging, and audit purposes.\n\n Attributes:\n job_id: Unique job identifier from job queue\n job_name: Name of the job function\n status: Current status (queued/running/success/failed)\n enqueued_at: When the job was added to queue\n started_at: When the job started executing\n completed_at: When the job finished\n error: Error message if job failed\n result: Job result data if successful\n\n**Source**: [job_log.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/job_log.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| job_id | str | \u2713 | Unique job identifier from job queue | minLen: 1 |\n| job_name | str | \u2713 | Name of the job function | minLen: 1 |\n| status | str | \u2713 | Job status: queued, running, success, or failed | - |\n| enqueued_at | datetime | | When the job was added to queue | - |\n| started_at | datetime | None | | When the job started executing | - |\n| completed_at | datetime | None | | When the job finished (success or failure) | - |\n| error | str | None | | Error message if job failed | - |\n| result | dict[str, Any] | None | | Job result data if successful | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| Employee | | | \u2713 | |\n| Manager | | | \u2713 | |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "JobLog"}} {"id": "doctype-activitylog", "type": "doctype", "title": "DocType: ActivityLog", "content": "# ActivityLog\n\nActivity log entry DocType.\n\n Stores an immutable audit record of a user action on a document.\n Once created, activity logs should never be modified or deleted.\n\n Attributes:\n user_id: ID of the user who performed the action\n action: Type of action (\"create\", \"read\", \"update\", \"delete\")\n doctype: Name of the affected DocType\n document_id: ID of the affected document\n timestamp: When the action occurred (auto-set)\n changes: Field changes for updates (old/new values)\n metadata: Additional context (request_id, ip, user_agent)\n\n Example:\n log = ActivityLog(\n user_id=\"user-001\",\n action=\"create\",\n doctype=\"Todo\",\n document_id=\"TODO-001\",\n )\n\n**Source**: [activity_log.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/activity_log.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| user_id | str | \u2713 | ID of the user who performed the action | minLen: 1 |\n| action | str | \u2713 | Type of action: create, read, update, delete | pattern: `^(create|read|update|delete)$` |\n| doctype | str | \u2713 | Name of the affected DocType | minLen: 1 |\n| document_id | str | \u2713 | ID of the affected document | minLen: 1 |\n| timestamp | datetime | | When the action occurred (UTC) | - |\n| changes | dict[str, Any] | None | | Field changes for updates: {field: {old: x, new: y}} | - |\n| metadata | dict[str, Any] | None | | Additional context: request_id, ip, user_agent, etc. | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| All | | | \u2713 | |\n| System Manager | \u2713 | | \u2713 | |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "ActivityLog"}} {"id": "doctype-report", "type": "doctype", "title": "DocType: Report", "content": "# Report\n\nCQRS report configuration.\n\n Defines reports that support zero-cliff scalability from\n startup (Code Reports) to enterprise (Analytics with OLAP).\n\n Attributes:\n name: Unique report identifier\n report_type: Report execution mode (Code Report or Analytics Report)\n data_source: Data source adapter (SQL, Elastic, ClickHouse)\n query: Report query (SQL file path, SQL query, or Elastic DSL)\n enabled: Whether the report is active\n\n**Source**: [report.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/report.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| name | str | \u2713 | Unique report identifier | minLen: 1, maxLen: 255 |\n| report_type | str | \u2713 | Report execution mode: 'Code Report' (indie) or 'Analytics Report' (enterprise) | - |\n| data_source | str | None | | Data source adapter: SQL, Elastic, ClickHouse (for Analytics Reports) | - |\n| query | str | None | | Report query: SQL file path, SQL query, or Elastic DSL JSON | - |\n| enabled | bool | | Whether the report is active | - |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| Admin | \u2713 | \u2713 | \u2713 | \u2713 |\n| Employee | | | \u2713 | |\n| Manager | \u2713 | | \u2713 | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "Report"}} {"id": "doctype-workflowtransition", "type": "doctype", "title": "DocType: WorkflowTransition", "content": "# WorkflowTransition\n\nDefines an allowed transition in a workflow.\n\n Attributes:\n workflow: Name of the workflow this transition belongs to\n from_state: Source state name\n to_state: Destination state name\n action: Action name that triggers this transition\n allowed_roles: List of roles permitted to perform this transition\n condition: Optional Python expression to evaluate before allowing transition\n\n**Source**: [workflow_transition.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/workflow_transition.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| workflow | str | | Workflow name | - |\n| from_state | str | | Source state | - |\n| to_state | str | | Destination state | - |\n| action | str | | Transition action name | - |\n| allowed_roles | list[str] | | Roles allowed to perform this transition | - |\n| condition | str | None | | Optional Python expression for conditional transitions | - |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "WorkflowTransition"}} {"id": "doctype-printformat", "type": "doctype", "title": "DocType: PrintFormat", "content": "# PrintFormat\n\nPrint format configuration DocType.\n\n Stores print template configuration for a DocType.\n\n Attributes:\n name: Display name for the format\n doctype: Target DocType this format applies to\n template: Path to Jinja2 template file\n is_default: If True, this is the default format for the DocType\n css: Optional custom CSS for styling\n header_html: Optional custom header HTML\n footer_html: Optional custom footer HTML\n page_size: Page size for PDF (A4, Letter, etc.)\n orientation: Page orientation (portrait, landscape)\n\n Example:\n format = PrintFormat(\n name=\"Invoice Pro Forma\",\n doctype=\"Invoice\",\n template=\"invoice_proforma.html\",\n )\n\n**Source**: [print_format.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/doctypes/print_format.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| name | str | \u2713 | Display name for the print format | minLen: 1, maxLen: 255 |\n| doctype | str | \u2713 | Target DocType this format applies to | minLen: 1 |\n| template | str | \u2713 | Path to Jinja2 template file | minLen: 1 |\n| is_default | bool | | If True, this is the default format for the DocType | - |\n| css | str | None | | Optional custom CSS for styling | - |\n| header_html | str | None | | Optional custom header HTML | - |\n| footer_html | str | None | | Optional custom footer HTML | - |\n| page_size | str | | Page size for PDF (A4, Letter, Legal) | pattern: `^(A4|Letter|Legal|A3|A5)$` |\n| orientation | str | | Page orientation (portrait, landscape) | pattern: `^(portrait|landscape)$` |\n\n## Permissions\n\n| Role | Create | Delete | Read | Write |\n|------|------|------|------|------|\n| All | | | \u2713 | |\n| System Manager | \u2713 | \u2713 | | \u2713 |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "PrintFormat"}} {"id": "doctype-namingcounter", "type": "doctype", "title": "DocType: NamingCounter", "content": "# NamingCounter\n\nCounter storage for naming series.\n\n Stores the current counter value for each prefix, enabling\n persistent and queryable counter management.\n\n Example:\n # Counter for INV-2026- prefix\n counter = NamingCounter(prefix=\"INV-2026-\", current=42)\n\n # Next invoice will be INV-2026-0043\n\n Note:\n Uses optimistic updates - no SELECT FOR UPDATE needed.\n On conflict, retry with incremented value.\n\n**Source**: [naming_counter.py](file:///builds/castlecraft/framework-m/libs/framework-m-core/src/framework_m_core/domain/naming_counter.py)\n\n## Fields\n\n| Field | Type | Required | Description | Validators |\n|-------|------|----------|-------------|------------|\n| prefix | str | | Naming series prefix | - |\n| current | int | | Current counter value | - |\n\n## Configuration\n\n| Setting | Value |\n|---------|-------|\n| Submittable | `False` |\n| Track Changes | `True` |\n\n## Controller\n\nController hooks are implemented in `*_controller.py` files.\nAvailable lifecycle hooks:\n\n- `validate()` - Called before save, raise exceptions for validation errors\n- `before_insert()` - Called before inserting a new document\n- `after_insert()` - Called after successfully inserting\n- `before_save()` - Called before saving (insert or update)\n- `after_save()` - Called after saving\n- `before_delete()` - Called before deleting\n- `after_delete()` - Called after deleting\n", "metadata": {"module": "framework_m_studio.discovery", "name": "NamingCounter"}} {"id": "file-docs-intro.md", "type": "doc", "title": "Document: docs/intro.md", "content": "---\nsidebar_position: 1\n---\n\n# Introduction\n\n[![Pipeline Status](https://gitlab.com/castlecraft/framework-m/badges/main/pipeline.svg)](https://gitlab.com/castlecraft/framework-m/-/pipelines)\n[![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/downloads/)\n[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Status: Pre-Alpha](https://img.shields.io/badge/status-pre--alpha-orange)](https://gitlab.com/castlecraft/framework-m)\n\nWelcome to the **Framework M** documentation.\n\n> **Build once. Scale forever.**\n\nFramework M is a modular, metadata-driven full-stack framework designed for building scalable enterprise applications. It bridges the gap between rapid development velocity and enterprise-grade architecture.\n\n:::tip Core Philosophical Pillars\nThe **\"M\"** in Framework M represents our three architectural foundations:\n\n- **Modular**: Built on **Hexagonal Architecture** (Ports & Adapters). Build a **Modular Monolith** for velocity and transparently decompose into **Microservices** for scale without a rewrite.\n- **Multi-Tenant**: Native, first-class support for multi-tenancy is baked into every layer\u2014from the database to the API.\n- **Metadata-Driven**: Application behavior is defined by **DocTypes** (metadata) to accelerate development. This enables automatic CRUD, UI, and workflows with modern type safety.\n:::\n\n## Why Framework M?\n\nBatteries-included frameworks deliver incredible initial velocity, but often lead to a \"productivity cliff\" when scale, enterprise integration, or complex deployment requirements arise.\n\nFramework M is designed to provide **Zero Cliff Architecture**. You get the same rapid development experience\u2014DocTypes, automatic CRUD, permissions, workflows\u2014but built on enterprise-grade foundations from day one.\n\n## Quick Navigation\n\n- **[Quick Start](user/getting-started.md)**: Get up and running in minutes.\n- **[Developer Guide](developer/index.md)**: Deep dive into the framework architecture.\n- **[Architecture](developer/architecture.md)**: Understand Hexagonal Architecture & Ports/Adapters.\n- **[Defining DocTypes](developer/defining-doctypes.md)**: Learn about metadata-driven development.\n- **[ADRs](adr/)**: Understand the \"why\" behind our architecture decisions.\n- **[Checklists](/checklists/)**: Track the implementation progress of the framework.\n\n---\n\n*For technical documentation and deployment details, please refer to the [README](https://gitlab.com/castlecraft/framework-m).*\n", "metadata": {"path": "docs/intro.md"}} {"id": "file-docs-developer-frontend-testing.md", "type": "doc", "title": "Document: docs/developer/frontend-testing.md", "content": "---\ntitle: Frontend Testing\nsidebar_position: 11\n---\n\n# Frontend Testing Guide\n\nComplete testing documentation for the Framework M frontend application.\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Test Infrastructure](#test-infrastructure)\n- [Unit Tests](#unit-tests)\n- [Integration Tests](#integration-tests)\n- [E2E Tests](#e2e-tests)\n- [Running Tests](#running-tests)\n- [Writing New Tests](#writing-new-tests)\n- [Best Practices](#best-practices)\n- [Troubleshooting](#troubleshooting)\n\n## Overview\n\nThe frontend uses a comprehensive testing strategy with three layers:\n\n1. **Unit Tests** - Test individual components in isolation\n2. **Integration Tests** - Test component interactions and workflows\n3. **E2E Tests** - Test complete user workflows across browsers\n\n### Technology Stack\n\n- **Test Runner**: [Vitest](https://vitest.dev/) - Vite-native test runner\n- **Component Testing**: [@testing-library/react](https://testing-library.com/react) - User-centric testing utilities\n- **User Interactions**: [@testing-library/user-event](https://testing-library.com/docs/user-event/intro) - Realistic user interactions\n- **Assertions**: [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) - DOM matchers\n- **DOM Environment**: [happy-dom](https://github.com/capricorn86/happy-dom) - Fast DOM implementation\n- **E2E Testing**: [Playwright](https://playwright.dev/) - Cross-browser automation\n\n## Test Infrastructure\n\n### Configuration Files\n\n#### `vitest.config.ts`\n\n```typescript\nimport { defineConfig } from \"vitest/config\";\nimport react from \"@vitejs/plugin-react\";\n\nexport default defineConfig({\n plugins: [react()],\n test: {\n environment: \"happy-dom\",\n globals: true,\n setupFiles: [\"./src/setupTests.ts\"],\n css: true,\n coverage: {\n provider: \"v8\",\n reporter: [\"text\", \"json\", \"html\"],\n },\n },\n});\n```\n\n#### `src/setupTests.ts`\n\nGlobal test setup that runs before all tests:\n\n- Imports `@testing-library/jest-dom` matchers\n- Mocks `window.matchMedia` (for theme detection)\n- Mocks `localStorage` and `sessionStorage`\n- Mocks `IntersectionObserver` and `ResizeObserver`\n- Auto-cleanup after each test\n\n#### `playwright.config.ts`\n\nE2E test configuration:\n\n- Tests in `tests/e2e/` directory\n- Runs against `http://localhost:5173`\n- Auto-starts dev server\n- Tests on Chromium, Firefox, and WebKit\n- Screenshots and videos on failure\n\n## Unit Tests\n\n### Location\n\nUnit tests are colocated with components in `__tests__` directories:\n\n```\nsrc/\n\u251c\u2500\u2500 components/\n\u2502 \u251c\u2500\u2500 __tests__/\n\u2502 \u2502 \u251c\u2500\u2500 ThemeToggle.test.tsx\n\u2502 \u2502 \u251c\u2500\u2500 AutoForm.test.tsx\n\u2502 \u2502 \u2514\u2500\u2500 AutoTable.test.tsx\n\u2502 \u251c\u2500\u2500 ThemeToggle.tsx\n\u2502 \u2514\u2500\u2500 ...\n```\n\n### Example: Component Test\n\n```typescript\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { ThemeToggle } from \"../ThemeToggle\";\nimport { ThemeProvider } from \"../../contexts/ThemeContext\";\n\ndescribe(\"ThemeToggle\", () => {\n it(\"toggles theme when clicked\", async () => {\n const user = userEvent.setup();\n\n render(\n \n \n \n );\n\n const button = screen.getByRole(\"button\");\n expect(button).toHaveAttribute(\"aria-label\", \"Switch to dark mode\");\n\n await user.click(button);\n\n expect(button).toHaveAttribute(\"aria-label\", \"Switch to light mode\");\n expect(window.localStorage.getItem(\"framework-m-theme\")).toBe(\"dark\");\n });\n});\n```\n\n### Covered Components\n\n#### `ThemeToggle.test.tsx` (6 tests)\n\n- \u2705 Renders toggle button\n- \u2705 Shows correct icon based on theme\n- \u2705 Toggles theme on click\n- \u2705 Persists to localStorage\n- \u2705 Applies dark class to document\n- \u2705 Keyboard accessible\n\n#### `AutoForm.test.tsx` (12 tests)\n\n- \u2705 Renders fields from JSON Schema\n- \u2705 Submit button visibility\n- \u2705 onChange/onSubmit callbacks\n- \u2705 Validation for required fields\n- \u2705 Pre-fills initial data\n- \u2705 Readonly and disabled modes\n- \u2705 Boolean fields as checkboxes\n- \u2705 Enum fields as selects\n\n#### `AutoTable.test.tsx` (11 tests)\n\n- \u2705 Renders columns from schema\n- \u2705 Displays data from Refine useList\n- \u2705 Loading state\n- \u2705 Row selection\n- \u2705 onRowClick callback\n- \u2705 Column sorting\n- \u2705 Pagination controls\n- \u2705 Formats booleans, dates, nulls\n\n## Integration Tests\n\n### Location\n\nIntegration tests are in `src/__tests__/integration/`:\n\n```\nsrc/\n\u2514\u2500\u2500 __tests__/\n \u2514\u2500\u2500 integration/\n \u251c\u2500\u2500 crud-flow.test.tsx\n \u2514\u2500\u2500 theme-persistence.test.tsx\n```\n\n### Example: CRUD Flow Test\n\n```typescript\ndescribe(\"CRUD Flow Integration\", () => {\n it(\"completes full CRUD workflow\", async () => {\n // 1. READ: List view shows records\n expect(screen.getByText(\"Test Todo 1\")).toBeInTheDocument();\n\n // 2. CREATE: Navigate to create form\n await user.click(screen.getByText(\"Create New\"));\n await user.click(screen.getByText(\"Save\"));\n expect(mockDataProvider.create).toHaveBeenCalled();\n\n // 3. UPDATE: Edit existing record\n await user.click(screen.getByText(\"Test Todo 1\"));\n await user.click(screen.getByText(\"Update\"));\n expect(mockDataProvider.update).toHaveBeenCalled();\n\n // 4. DELETE: Remove record\n await user.click(screen.getByText(\"Delete\"));\n expect(mockDataProvider.deleteOne).toHaveBeenCalled();\n });\n});\n```\n\n### Covered Workflows\n\n#### `crud-flow.test.tsx` (3 tests)\n\n- \u2705 Full CRUD workflow (Create \u2192 Read \u2192 Update \u2192 Delete)\n- \u2705 Validation errors during create\n- \u2705 Network error handling\n\n#### `theme-persistence.test.tsx` (7 tests)\n\n- \u2705 Theme persists to localStorage\n- \u2705 Restores theme on mount\n- \u2705 Applies dark class to document\n- \u2705 System preference detection\n- \u2705 Saved theme overrides system\n- \u2705 System preference change listener\n- \u2705 Theme consistency across components\n\n## E2E Tests\n\n### Location\n\nE2E tests are in `tests/e2e/`:\n\n```\ntests/\n\u2514\u2500\u2500 e2e/\n \u251c\u2500\u2500 login.spec.ts\n \u251c\u2500\u2500 create-record.spec.ts\n \u2514\u2500\u2500 cross-browser.spec.ts\n```\n\n### Example: Login Test\n\n```typescript\nimport { test, expect } from \"@playwright/test\";\n\ntest.describe(\"User Login\", () => {\n test(\"should successfully log in\", async ({ page }) => {\n await page.goto(\"/login\");\n\n await page.getByLabel(/email/i).fill(\"test@example.com\");\n await page.getByLabel(/password/i).fill(\"password123\");\n await page.getByRole(\"button\", { name: /login/i }).click();\n\n await expect(page).toHaveURL(/\\/app\\/dashboard/);\n await expect(page.getByRole(\"button\", { name: /logout/i })).toBeVisible();\n });\n});\n```\n\n### Covered Scenarios\n\n#### `login.spec.ts` (7 tests)\n\n- \u2705 Display login form\n- \u2705 Validation errors for empty form\n- \u2705 Error for invalid credentials\n- \u2705 Successful login\n- \u2705 Session persistence on reload\n- \u2705 Logout functionality\n- \u2705 Keyboard navigation\n\n#### `create-record.spec.ts` (9 tests)\n\n- \u2705 Navigate to create form\n- \u2705 Display form fields from metadata\n- \u2705 Required field validation\n- \u2705 Create record successfully\n- \u2705 Cancel creation\n- \u2705 Network error handling\n- \u2705 Edit after creation\n- \u2705 Auto-save drafts\n- \u2705 Real-time validation\n\n#### `cross-browser.spec.ts` (10 tests)\n\n- \u2705 Theme toggle across browsers\n- \u2705 Navigation between views\n- \u2705 Search functionality\n- \u2705 Bulk actions\n- \u2705 Form validation consistency\n- \u2705 Locale switching\n- \u2705 Responsive design (mobile)\n- \u2705 Keyboard shortcuts\n- \u2705 Page transitions\n- \u2705 Error states\n\n## Running Tests\n\n### Unit & Integration Tests\n\n```bash\n# Run all tests once\npnpm test --run\n\n# Run tests in watch mode\npnpm test\n\n# Run with UI\npnpm test:ui\n\n# Run with coverage\npnpm test:coverage\n```\n\n### E2E Tests\n\n```bash\n# Run all E2E tests\npnpm test:e2e\n\n# Run with Playwright UI\npnpm test:e2e:ui\n\n# Run in headed mode (see browser)\npnpm test:e2e:headed\n\n# Run specific browser\npnpm test:e2e --project=chromium\npnpm test:e2e --project=firefox\npnpm test:e2e --project=webkit\n\n# Run specific test file\npnpm test:e2e tests/e2e/login.spec.ts\n```\n\n### CI/CD\n\nTests run automatically in CI:\n\n```yaml\n# .github/workflows/test.yml\n- name: Run unit tests\n run: pnpm test --run\n\n- name: Run E2E tests\n run: pnpm test:e2e\n```\n\n## Writing New Tests\n\n### Unit Test Template\n\n```typescript\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { YourComponent } from \"../YourComponent\";\n\ndescribe(\"YourComponent\", () => {\n it(\"should do something\", async () => {\n const user = userEvent.setup();\n const handleClick = vi.fn();\n\n render();\n\n const button = screen.getByRole(\"button\");\n await user.click(button);\n\n expect(handleClick).toHaveBeenCalled();\n });\n});\n```\n\n### Integration Test Template\n\n```typescript\nimport { describe, it, expect, beforeEach } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { BrowserRouter } from \"react-router\";\nimport { Refine } from \"@refinedev/core\";\n\ndescribe(\"Feature Integration\", () => {\n beforeEach(() => {\n // Setup test environment\n });\n\n it(\"completes workflow\", async () => {\n render(\n \n \n \n \n \n );\n\n // Test workflow steps\n await waitFor(() => {\n expect(screen.getByText(\"Expected Result\")).toBeInTheDocument();\n });\n });\n});\n```\n\n### E2E Test Template\n\n```typescript\nimport { test, expect } from \"@playwright/test\";\n\ntest.describe(\"Feature E2E\", () => {\n test.beforeEach(async ({ page }) => {\n await page.goto(\"/your-page\");\n });\n\n test(\"should work as expected\", async ({ page }) => {\n await page.getByRole(\"button\", { name: /action/i }).click();\n await expect(page).toHaveURL(/expected-url/);\n await expect(page.getByText(\"Success\")).toBeVisible();\n });\n});\n```\n\n## Best Practices\n\n### General\n\n1. **Test User Behavior**: Test what users see and do, not implementation details\n2. **Accessible Queries**: Use `getByRole`, `getByLabelText` over `getByTestId`\n3. **Async Actions**: Always `await` user interactions\n4. **Isolation**: Each test should be independent\n5. **Cleanup**: Use `beforeEach` and `afterEach` for setup/teardown\n\n### Unit Tests\n\n- Mock external dependencies (API calls, contexts)\n- Test one component at a time\n- Cover edge cases (empty state, errors, loading)\n- Test accessibility (ARIA attributes, keyboard navigation)\n\n### Integration Tests\n\n- Test component interactions\n- Use real providers when possible\n- Mock only external APIs\n- Test complete workflows\n\n### E2E Tests\n\n- Test critical user journeys\n- Use realistic test data\n- Handle timing issues with `waitFor`\n- Test across browsers\n- Keep tests fast (avoid unnecessary waits)\n\n## Troubleshooting\n\n### Common Issues\n\n#### \"Cannot find module\" errors\n\n```bash\n# Install dependencies\npnpm install\n```\n\n#### Tests timeout\n\n```typescript\n// Increase timeout for slow tests\ntest(\"slow test\", async () => {\n // ...\n}, 10000); // 10 seconds\n```\n\n#### \"Element not found\" errors\n\n```typescript\n// Wait for element to appear\nawait waitFor(() => {\n expect(screen.getByText(\"Hello\")).toBeInTheDocument();\n});\n\n// Or use findBy (waits automatically)\nconst element = await screen.findByText(\"Hello\");\n```\n\n#### E2E tests fail locally\n\n```bash\n# Ensure dev server is running\npnpm dev\n\n# Or let Playwright start it automatically (configured in playwright.config.ts)\npnpm test:e2e\n```\n\n#### Browser not installed\n\n```bash\n# Install Playwright browsers\npnpm exec playwright install\n```\n\n### Debug Mode\n\n#### Vitest\n\n```bash\n# Run tests in debug mode\npnpm test --inspect-brk\n\n# Open Chrome DevTools \u2192 chrome://inspect\n```\n\n#### Playwright\n\n```bash\n# Run with UI mode (recommended)\npnpm test:e2e:ui\n\n# Run in headed mode\npnpm test:e2e:headed\n\n# Debug specific test\npnpm exec playwright test --debug login.spec.ts\n```\n\n### Coverage Reports\n\n```bash\n# Generate coverage report\npnpm test:coverage\n\n# Open HTML report\nopen coverage/index.html\n```\n\nCoverage is configured in `vitest.config.ts`:\n\n- Text output to terminal\n- JSON for CI integration\n- HTML for detailed viewing\n\n### CI/CD Integration\n\nTests run on every push and PR:\n\n- Unit tests: Fast feedback (< 30s)\n- E2E tests: Comprehensive validation (2-5 min)\n- Coverage reports uploaded to CI\n\n## Test Statistics\n\nCurrent test coverage:\n\n- **Unit Tests**: 29 tests across 3 files\n- **Integration Tests**: 10 tests across 2 files\n- **E2E Tests**: 26 tests across 3 files\n- **Total**: 65 tests\n\n### Coverage Metrics\n\n- Components: 85%+ coverage\n- Critical paths: 100% coverage\n- Edge cases: Well tested\n\n## Next Steps\n\nTo improve test coverage:\n\n1. Add tests for remaining components (LocaleSwitcher, WorkflowActions, etc.)\n2. Increase integration test scenarios\n3. Add visual regression tests (Playwright + Snapshots)\n4. Performance testing (Lighthouse CI)\n5. Accessibility testing (axe-core integration)\n\n## References\n\n- [Vitest Documentation](https://vitest.dev/)\n- [Testing Library](https://testing-library.com/)\n- [Playwright Documentation](https://playwright.dev/)\n- [React Testing Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)\n", "metadata": {"path": "docs/developer/frontend-testing.md"}} {"id": "file-docs-developer-studio-setup.md", "type": "doc", "title": "Document: docs/developer/studio-setup.md", "content": "---\ntitle: Studio Setup\nsidebar_position: 10\n---\n\n# Studio Setup Guide\n\n## Running Studio\n\nThe Studio provides a visual interface for designing DocTypes and managing your Framework M application.\n\n### Single Command\n\n```bash\nm studio --port 9999\n```\n\nThis starts the Studio server with both the UI and API on a single port.\n\n### URL Structure\n\nThe Studio uses a **path prefix architecture** for clean separation:\n\n- **UI**: `http://127.0.0.1:9999/studio/ui/`\n - Main interface for designing DocTypes\n - All frontend routes are under `/studio/ui/*`\n\n- **API**: `http://127.0.0.1:9999/studio/api/`\n - Backend endpoints for data operations\n - All API routes are under `/studio/api/*`\n\n- **Root**: `http://127.0.0.1:9999/`\n - Automatically redirects to `/studio/ui/`\n\n### Development Mode\n\nFor frontend development with hot reload:\n\n**Terminal 1 - API Server:**\n\n```bash\nm studio --port 9999\n```\n\n**Terminal 2 - Frontend Dev Server:**\n\n```bash\ncd apps/studio/studio_ui\npnpm dev\n```\n\nThe Vite dev server runs on `http://localhost:5173` with API proxying to port 9999.\n\n### Git Noise and Local Builds\n\n**Avoid running `pnpm build` locally during development.**\n\nRunning `pnpm build` updates the static assets in `apps/studio/src/framework_m_studio/static/`. Since Vite includes hashes in filenames (e.g., `index-a1b2c3.js`), every build modifies tracked files, creating unnecessary git noise.\n\n**Best Practice:**\n\n- Use `pnpm dev` for frontend development.\n- **Rely on CI/CD** for release builds. The pipeline automatically rebuilds the UI assets for PyPI packages.\n- Local `pnpm build` is only necessary to verify production artifacts locally or update the repository snapshot.\n\n## Architecture\n\n### Path Prefix Separation\n\nThe Studio implements complete separation between UI and API routes:\n\n```\n/studio/ui/* \u2192 React SPA (HTML pages)\n/studio/api/* \u2192 JSON API endpoints\n/studio \u2192 Redirects to /studio/ui/\n```\n\nThis architecture:\n\n- \u2705 Prevents route collisions\n- \u2705 Enables clean URLs (no hash fragments)\n- \u2705 Works with single-command deployment\n- \u2705 Supports proper Content-Type headers\n\n### Static Assets\n\nStatic assets (JS, CSS, images) are served at:\n\n```\n/studio/ui/assets/*\n```\n\nThe Vite build process outputs to `apps/studio/src/framework_m_studio/static/` which is included in the Python package.\n\n## API Endpoints\n\n### Health Check\n\n```bash\ncurl http://127.0.0.1:9999/studio/api/health\n```\n\n### List DocTypes\n\n```bash\ncurl http://127.0.0.1:9999/studio/api/doctypes\n```\n\n### Get DocType\n\n```bash\ncurl http://127.0.0.1:9999/studio/api/doctype/User\n```\n\n### Field Types\n\n```bash\ncurl http://127.0.0.1:9999/studio/api/field-types\n```\n\n## Building for Production\n\n### Build Frontend\n\n```bash\ncd apps/studio/studio_ui\npnpm build\n```\n\nThis outputs to `../src/framework_m_studio/static/` which is packaged with the Python distribution.\n\n### Package Python\n\n```bash\ncd apps/studio\nuv build\n```\n\nThe built wheel includes the static frontend assets.\n\n## Troubleshooting\n\n### UI Downloads Instead of Rendering\n\nIf the browser downloads HTML files instead of rendering them, check:\n\n1. **Content-Type Headers**: Ensure responses have `Content-Type: text/html; charset=utf-8`\n2. **Route Handlers**: Verify return type is `Response` not union types\n3. **Browser Cache**: Clear browser cache and hard reload (Ctrl+Shift+R)\n\n### 404 Errors on Deep Links\n\nIf refreshing a page like `/studio/ui/doctypes/edit/User` gives 404:\n\n1. **Check Route Handler**: Ensure `/studio/ui/{path:path}` wildcard route exists\n2. **SPA Fallback**: Verify the handler returns `index.html` for non-file paths\n3. **Base Path**: Confirm React Router uses `basename=\"/studio/ui\"`\n\n### API Calls Failing\n\nIf API calls return errors:\n\n1. **CORS**: Check CORS configuration allows your origin\n2. **Path Prefix**: Ensure API calls use `/studio/api/*` prefix\n3. **Server Running**: Verify `m studio` is running on the correct port\n", "metadata": {"path": "docs/developer/studio-setup.md"}} {"id": "file-docs-developer-architecture.md", "type": "doc", "title": "Document: docs/developer/architecture.md", "content": "# Architecture\n\nA guide to understanding Framework M's architecture and project structure.\n\n## Overview\n\nFramework M uses **Hexagonal Architecture** (Ports & Adapters) to maintain clean separation between business logic and infrastructure.\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Primary Adapters \u2502\n\u2502 (HTTP, CLI, Background Jobs) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Core Domain \u2502\n \u2502 (DocTypes, Logic) \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Secondary Adapters \u2502\n\u2502 (Database, Cache, Storage, Events, etc.) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Project Structure (Monorepo)\n\nFramework M is split into focused packages to enable flexibility and lean production deployments.\n\n### Core Packages (`libs/`)\n\n1. **`framework-m-core`**: The absolute minimum kernel.\n - All **Protocols** (Interfaces) for Repository, EventBus, Cache, etc.\n - Pydantic-based DocType metadata engine.\n - Dependency Injection container.\n - **Zero adapters** (infrastructure-free).\n\n2. **`framework-m-standard`**: The standard battery of adapters.\n - **SQLAlchemy** adapters (PostgreSQL/SQLite).\n - **NATS** adapters for Event Bus and WebSockets.\n - **Redis** adapters for Caching.\n - **Litestar** web integration.\n - RBAC Permission system.\n\n3. **`framework-m`**: The meta-package.\n - Bundles `core` + `standard`.\n - Recommended for most developers and \"Indie\" deployments.\n\n### Developer Tools (`apps/`)\n\n- **`framework-m-studio`**: Visual DocType builder and CLI scaffolding.\n - Contains `m new:app`, `m new:doctype`.\n - Documentation generators.\n - Studio UI (React + Refine).\n\n---\n\n## Detailed Directory Map\n\n```\nlibs/\n\u251c\u2500\u2500 framework-m-core/\n\u2502 \u2514\u2500\u2500 src/framework_m_core/\n\u2502 \u251c\u2500\u2500 domain/ # Base protocols\n\u2502 \u251c\u2500\u2500 interfaces/ # Protocol definitions (Ports)\n\u2502 \u2514\u2500\u2500 registry.py # MetaRegistry\n\u2502\n\u251c\u2500\u2500 framework-m-standard/\n\u2502 \u2514\u2500\u2500 src/framework_m_standard/\n\u2502 \u251c\u2500\u2500 adapters/ # Concrete implementations\n\u2502 \u2502 \u251c\u2500\u2500 db/ # SQLAlchemy\n\u2502 \u2502 \u251c\u2500\u2500 eventbus/ # NATS\n\u2502 \u2502 \u2514\u2500\u2500 web/ # Litestar\n\u2502 \u2514\u2500\u2500 cli/ # Standard commands (migrate, start)\n\u2502\napps/\n\u2514\u2500\u2500 studio/ # Studio UI & Scaffolding\n \u2514\u2500\u2500 src/framework_m_studio/\n \u251c\u2500\u2500 cli/ # Dev commands (new, codegen, docs)\n \u2514\u2500\u2500 docs_generator.py # Documentation engine\n```\n\n## Key Modules\n\n| Module | Purpose | Location |\n|------|---------|----------|\n| `framework_m_core.domain` | Base classes for DocTypes/Controllers | `framework-m-core` |\n| `framework_m_core.interfaces` | System-wide Protocols (contracts) | `framework-m-core` |\n| `framework_m_standard.adapters.db` | SQLAlchemy & GenericRepository | `framework-m-standard` |\n| `framework_m_standard.adapters.web` | Litestar app factory & Auto-CRUD | `framework-m-standard` |\n| `framework_m_studio.cli.new` | Code generation and app templates | `framework-m-studio` |\n\n## Protocol-Based Design\n\nFramework M defines **Protocols** (interfaces) for all infrastructure concerns:\n\n- See [Protocol Reference](../developer/generated/index.md)\n\nThis allows you to:\n1. **Swap implementations** without changing business logic\n2. **Test in isolation** with mock adapters\n3. **Deploy differently** (e.g., Redis vs memory cache)\n\n## Controller Hooks\n\nBusiness logic lives in Controllers, not DocTypes. See [Defining DocTypes](./defining-doctypes.md) for hook examples.\n\n## CLI Commands\n\nFramework M provides CLI tools for development. Run `m --help` for available commands.\n\n## Apps Structure\n\nUser applications live in `apps/`:\n\n```\nyour_app/\n\u251c\u2500\u2500 src/\n\u2502 \u2514\u2500\u2500 your_app/ # Namespaced package\n\u2502 \u2514\u2500\u2500 doctypes/\n\u2502 \u2514\u2500\u2500 your_doctype/\n\u2502 \u251c\u2500\u2500 __init__.py\n\u2502 \u251c\u2500\u2500 doctype.py # Schema\n\u2502 \u2514\u2500\u2500 controller.py # Business logic\n\u251c\u2500\u2500 pyproject.toml\n\u2514\u2500\u2500 README.md\n```\n\n## Next Steps\n\n- [Creating Apps](./creating-apps.md) - Build your first app\n- [Defining DocTypes](./defining-doctypes.md) - Advanced patterns\n", "metadata": {"path": "docs/developer/architecture.md"}} {"id": "file-docs-developer-plugin-architecture-implementation.md", "type": "doc", "title": "Document: docs/developer/plugin-architecture-implementation.md", "content": "# Plugin Architecture Implementation Guide\n\n> **Related Documents:**\n>\n> - [ADR-0008: Frontend Plugin Architecture](../adr/0008-frontend-plugin-architecture.md) - High-level design decision\n> - [Framework M Sidebar Architecture](../rfcs/rfc-0007-sidebar-architecture.md) - Sidebar implementation that consumes plugin menus\n\n## Table of Contents\n\n1. [Why Do We Need a Plugin SDK?](#why-do-we-need-a-plugin-sdk)\n2. [Package Roles & Responsibilities](#package-roles--responsibilities)\n3. [Plugin Menu Implementation](#plugin-menu-implementation)\n4. [Step-by-Step Implementation Plan](#step-by-step-implementation-plan)\n5. [Developer Workflow Examples](#developer-workflow-examples)\n\n---\n\n## Why Do We Need a Plugin SDK?\n\n### The Problem Without an SDK\n\nCurrently, if you want to build a WMS (Warehouse Management) app with custom frontend:\n\n```bash\n# Without SDK - Manual wiring required\nm new:app wms\ncd frontend/\n# Now what? How do I add WMS routes/menus without forking the entire frontend?\n# Answer: You can't. You have to modify frontend/src/ directly.\n```\n\n**Problems:**\n\n1. \u274c **No separation** - WMS code mixed with core framework code\n2. \u274c **No reusability** - Can't share WMS plugin across projects\n3. \u274c **Merge conflicts** - Multiple apps editing same files\n4. \u274c **No discovery** - Manual imports everywhere\n5. \u274c **Tight coupling** - Changes to one app affect others\n\n### The Solution With an SDK\n\n```bash\n# With SDK - Plugin auto-discovery\nm new:app wms --with-frontend\ncd frontend/\npnpm dev # WMS plugin automatically loaded, routes/menus registered\n\n# Later, add another app\nm new:app personnel --with-frontend\npnpm dev # Both WMS + Personnel auto-discovered and composed\n```\n\n**Benefits:**\n\n1. \u2705 **Clean separation** - Each app is self-contained plugin\n2. \u2705 **Reusable** - Share plugins across projects\n3. \u2705 **No conflicts** - Each app in its own directory\n4. \u2705 **Auto-discovery** - Vite plugin finds all workspace plugins\n5. \u2705 **Loose coupling** - Apps don't know about each other\n\n### What the SDK Provides\n\nThe SDK is the **contract and tooling** that makes plugins possible:\n\n```typescript\n// Without SDK - You write this manually everywhere:\nimport WMSRoutes from \"../apps/wms/routes\";\nimport PersonnelRoutes from \"../apps/personnel/routes\";\nimport FinanceRoutes from \"../apps/finance/routes\";\n\nconst routes = [\n ...WMSRoutes,\n ...PersonnelRoutes,\n ...FinanceRoutes,\n // Every new app = manual import + spread\n];\n\n// With SDK - Framework does this automatically:\nconst registry = new PluginRegistry();\nawait registry.discoverAndRegister(); // Finds all plugins\nconst routes = registry.getRoutes(); // Merged automatically\nconst menus = registry.getMenu(); // Merged automatically\n```\n\n**The SDK is the \"glue\" that:**\n\n- Defines plugin structure (types, interfaces)\n- Discovers plugins at build time\n- Registers plugins into a central registry\n- Provides React hooks for accessing plugin data\n- Manages plugin lifecycle\n\n---\n\n## Package Roles & Responsibilities\n\n### Current Package Landscape\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 External Packages (npm) \u2502\n\u2502 - @refinedev/core, @refinedev/react-router, etc. \u2502\n\u2502 - react, react-dom, react-router-dom \u2502\n\u2502 - @tanstack/react-query, lucide-react, etc. \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 used by\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 @framework-m/desk (EXISTING) \u2502\n\u2502 Location: libs/framework-m-desk/ \u2502\n\u2502 \u2502\n\u2502 Purpose: Core UI library for Framework M applications \u2502\n\u2502 \u2502\n\u2502 Responsibilities: \u2502\n\u2502 \u2022 Refine.dev data provider (REST API integration) \u2502\n\u2502 \u2022 Auth provider (JWT, session management) \u2502\n\u2502 \u2022 Live provider (WebSocket notifications) \u2502\n\u2502 \u2022 Base DocType components (List, Form, Show) \u2502\n\u2502 \u2022 Hooks (useDocType, useMetadata, useWorkflow) \u2502\n\u2502 \u2022 Utilities (date formatting, validation, etc.) \u2502\n\u2502 \u2502\n\u2502 Consumers: Both single-app and multi-app projects \u2502\n\u2502 Published to: GitLab npm registry (@framework-m/desk) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n### New Packages for Plugin System\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 @framework-m/plugin-sdk (NEW - Phase 1) \u2502\n\u2502 Location: libs/framework-m-plugin-sdk/ \u2502\n\u2502 \u2502\n\u2502 Purpose: Plugin infrastructure and runtime \u2502\n\u2502 \u2502\n\u2502 Responsibilities: \u2502\n\u2502 \u2022 PluginRegistry - Central registry for all plugins \u2502\n\u2502 \u2022 ServiceContainer - Dependency injection for services \u2502\n\u2502 \u2022 React hooks - usePlugin(), useService(), usePluginMenu() \u2502\n\u2502 \u2022 Types - FrameworkMPlugin interface, MenuItem, etc. \u2502\n\u2502 \u2022 Discovery - discoverPlugins() utility \u2502\n\u2502 \u2502\n\u2502 Dependencies: React (peer), @framework-m/desk (peer) \u2502\n\u2502 Consumers: Shell app, all plugin packages \u2502\n\u2502 Published to: GitLab npm registry \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 used by\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 @framework-m/vite-plugin (NEW - Phase 2) \u2502\n\u2502 Location: libs/framework-m-vite-plugin/ \u2502\n\u2502 \u2502\n\u2502 Purpose: Build-time plugin discovery and bundling \u2502\n\u2502 \u2502\n\u2502 Responsibilities: \u2502\n\u2502 \u2022 Scan workspace for packages with \"framework-m\" metadata \u2502\n\u2502 \u2022 Generate virtual entry point with all plugins \u2502\n\u2502 \u2022 Configure code-splitting (one chunk per plugin) \u2502\n\u2502 \u2022 Setup HMR for plugin hot reload \u2502\n\u2502 \u2022 Type generation for discovered plugins \u2502\n\u2502 \u2502\n\u2502 Dependencies: vite, @framework-m/plugin-sdk \u2502\n\u2502 Consumers: Shell app vite.config.ts \u2502\n\u2502 Published to: GitLab npm registry \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 used by\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 frontend/ (Shell Application) \u2502\n\u2502 Location: frontend/ (root of project) \u2502\n\u2502 \u2502\n\u2502 Purpose: Composition root - assembles all plugins \u2502\n\u2502 \u2502\n\u2502 Responsibilities: \u2502\n\u2502 \u2022 Bootstrap PluginRegistry \u2502\n\u2502 \u2022 Render composed UI (routes, menus, providers) \u2502\n\u2502 \u2022 Provide base layout (Sidebar, Header, etc.) \u2502\n\u2502 \u2022 Handle authentication flow \u2502\n\u2502 \u2022 Error boundaries and loading states \u2502\n\u2502 \u2502\n\u2502 Dependencies: \u2502\n\u2502 \u2022 @framework-m/desk - Core UI components \u2502\n\u2502 \u2022 @framework-m/plugin-sdk - Plugin runtime \u2502\n\u2502 \u2022 @framework-m/vite-plugin - Build tooling \u2502\n\u2502 \u2502\n\u2502 NOT published - Static build output (dist/) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 uses\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 App Plugins (Workspace Packages) \u2502\n\u2502 Locations: \u2502\n\u2502 \u2022 apps/wms/frontend/ \u2502\n\u2502 \u2022 apps/personnel/frontend/ \u2502\n\u2502 \u2022 apps/finance/frontend/ \u2502\n\u2502 \u2502\n\u2502 Purpose: Domain-specific UI modules \u2502\n\u2502 \u2502\n\u2502 Example: @my-company/wms \u2502\n\u2502 \u2502\n\u2502 Responsibilities: \u2502\n\u2502 \u2022 Define routes for WMS screens \u2502\n\u2502 \u2022 Define menu items for WMS navigation \u2502\n\u2502 \u2022 Provide WMS-specific components \u2502\n\u2502 \u2022 Register WMS services (WarehouseService, etc.) \u2502\n\u2502 \u2022 Extend base DocTypes (add WMS-specific fields) \u2502\n\u2502 \u2502\n\u2502 Dependencies: \u2502\n\u2502 \u2022 @framework-m/desk - Use base components \u2502\n\u2502 \u2022 @framework-m/plugin-sdk - Plugin contract \u2502\n\u2502 \u2502\n\u2502 NOT published to npm - Workspace-local only \u2502\n\u2502 Bundled into frontend/dist/ at build time \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n### Package Dependency Matrix\n\n| Package | Depends On | Used By | Published? |\n| -------------------------- | ---------------------------- | --------------------------- | -------------------- |\n| `@framework-m/desk` | `@refinedev/*`, `react` | Shell, All Plugins | \u2705 GitLab npm |\n| `@framework-m/plugin-sdk` | `react`, `@framework-m/desk` | Shell, Plugins, Vite Plugin | \u2705 GitLab npm |\n| `@framework-m/vite-plugin` | `vite`, `plugin-sdk` | Shell (vite.config.ts) | \u2705 GitLab npm |\n| Shell App (`frontend/`) | All framework packages | - | \u274c Static build only |\n| App Plugins | `desk`, `plugin-sdk` | Shell (via auto-discovery) | \u274c Workspace-local |\n\n### Key Design Principles\n\n1. **@framework-m/desk remains unchanged** - No breaking changes for existing users\n2. **Plugin SDK is opt-in** - Single-app users don't need it\n3. **Workspace-local plugins** - No need to publish app-specific code\n4. **Build-time composition** - Plugins bundled into single artifact\n5. **Runtime flexibility** - Can switch to Module Federation later if needed\n\n---\n\n## Plugin Menu Implementation\n\n### Step 1: Define Plugin Contract (in SDK)\n\n```typescript\n// libs/framework-m-plugin-sdk/src/types/plugin.ts\n\nexport interface MenuItem {\n name: string; // Unique ID: \"sales.invoice\"\n label: string; // Display: \"Sales Invoice\"\n route: string; // Path: \"/doctypes/sales.invoice\"\n icon?: string; // Icon name: \"file-text\"\n module?: string; // Group: \"Sales\"\n category?: string; // Subgroup: \"Transactions\"\n order?: number; // Sort order\n children?: MenuItem[]; // Nested items\n}\n\nexport interface FrameworkMPlugin {\n name: string;\n version: string;\n\n // Menu contribution\n menu?: MenuItem[];\n\n // Other plugin features\n routes?: RouteObject[];\n services?: Record;\n providers?: Provider[];\n doctypes?: DocTypeExtension[];\n widgets?: Widget[];\n}\n```\n\n### Step 2: Create Plugin Registry (in SDK)\n\n```typescript\n// libs/framework-m-plugin-sdk/src/core/PluginRegistry.ts\n\nimport { FrameworkMPlugin, MenuItem } from \"../types/plugin\";\n\nexport class PluginRegistry {\n private plugins = new Map();\n private menuCache: MenuItem[] | null = null;\n\n /**\n * Register a plugin with the registry\n */\n async register(plugin: FrameworkMPlugin): Promise {\n // Validate plugin structure\n if (!plugin.name || !plugin.version) {\n throw new Error(\"Plugin must have name and version\");\n }\n\n // Check for duplicates\n if (this.plugins.has(plugin.name)) {\n console.warn(`Plugin ${plugin.name} already registered, overwriting`);\n }\n\n this.plugins.set(plugin.name, plugin);\n this.menuCache = null; // Invalidate cache\n }\n\n /**\n * Get merged menu tree from all plugins\n */\n getMenu(): MenuItem[] {\n if (this.menuCache) return this.menuCache;\n\n const allMenus: MenuItem[] = [];\n\n // Collect menu items from all plugins\n for (const plugin of this.plugins.values()) {\n if (plugin.menu) {\n allMenus.push(...plugin.menu);\n }\n }\n\n // Merge and sort menu items\n this.menuCache = this.mergeMenus(allMenus);\n return this.menuCache;\n }\n\n /**\n * Merge menu items by module and category\n */\n private mergeMenus(items: MenuItem[]): MenuItem[] {\n const moduleMap = new Map();\n\n for (const item of items) {\n const moduleName = item.module || \"Other\";\n\n if (!moduleMap.has(moduleName)) {\n // Create module group\n moduleMap.set(moduleName, {\n name: moduleName.toLowerCase(),\n label: moduleName,\n route: `/${moduleName.toLowerCase()}`,\n icon: this.getModuleIcon(moduleName),\n children: [],\n });\n }\n\n const moduleGroup = moduleMap.get(moduleName)!;\n\n if (item.category) {\n // Find or create category subgroup\n let categoryGroup = moduleGroup.children?.find(\n c => c.name === item.category,\n );\n\n if (!categoryGroup) {\n categoryGroup = {\n name: item.category!.toLowerCase(),\n label: item.category!,\n route: `/${moduleName.toLowerCase()}/${item.category!.toLowerCase()}`,\n children: [],\n };\n moduleGroup.children!.push(categoryGroup);\n }\n\n categoryGroup.children!.push(item);\n } else {\n // No category, add directly to module\n moduleGroup.children!.push(item);\n }\n }\n\n // Convert map to array and sort\n return Array.from(moduleMap.values()).sort(\n (a, b) => (a.order || 999) - (b.order || 999),\n );\n }\n\n private getModuleIcon(moduleName: string): string {\n const icons: Record = {\n Sales: \"shopping-cart\",\n Inventory: \"package\",\n HR: \"users\",\n Finance: \"dollar-sign\",\n Core: \"settings\",\n };\n return icons[moduleName] || \"folder\";\n }\n\n /**\n * Get all registered routes\n */\n getRoutes(): RouteObject[] {\n const routes: RouteObject[] = [];\n for (const plugin of this.plugins.values()) {\n if (plugin.routes) {\n routes.push(...plugin.routes);\n }\n }\n return routes;\n }\n\n /**\n * Get all registered plugins\n */\n getAllPlugins(): FrameworkMPlugin[] {\n return Array.from(this.plugins.values());\n }\n}\n```\n\n### Step 3: Create React Hook (in SDK)\n\n```typescript\n// libs/framework-m-plugin-sdk/src/hooks/usePluginMenu.ts\n\nimport { useContext, useMemo } from \"react\";\nimport { PluginRegistryContext } from \"../context/PluginRegistryContext\";\nimport { MenuItem } from \"../types/plugin\";\n\n/**\n * Hook to access the merged menu tree from all plugins\n */\nexport function usePluginMenu(): MenuItem[] {\n const registry = useContext(PluginRegistryContext);\n\n if (!registry) {\n throw new Error(\"usePluginMenu must be used within PluginRegistryProvider\");\n }\n\n return useMemo(() => registry.getMenu(), [registry]);\n}\n\n/**\n * Hook to access a specific plugin's data\n */\nexport function usePlugin(name: string) {\n const registry = useContext(PluginRegistryContext);\n\n if (!registry) {\n throw new Error(\"usePlugin must be used within PluginRegistryProvider\");\n }\n\n return useMemo(\n () => registry.getAllPlugins().find(p => p.name === name),\n [registry, name],\n );\n}\n```\n\n### Step 4: Create Context Provider (in SDK)\n\n```typescript\n// libs/framework-m-plugin-sdk/src/context/PluginRegistryContext.tsx\n\nimport { createContext, ReactNode, useEffect, useState } from 'react';\nimport { PluginRegistry } from '../core/PluginRegistry';\n\nexport const PluginRegistryContext = createContext(null);\n\ninterface PluginRegistryProviderProps {\n children: ReactNode;\n registry?: PluginRegistry;\n}\n\nexport function PluginRegistryProvider({\n children,\n registry: externalRegistry,\n}: PluginRegistryProviderProps) {\n const [registry] = useState(() => externalRegistry || new PluginRegistry());\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n // Mark as ready after initial mount\n setIsReady(true);\n }, []);\n\n if (!isReady) {\n return
Loading plugins...
;\n }\n\n return (\n \n {children}\n \n );\n}\n```\n\n### Step 5: Update Shell App to Use Plugin Menu\n\n```typescript\n// frontend/src/layout/Sidebar.tsx\n\nimport { usePluginMenu } from '@framework-m/plugin-sdk';\nimport { useMemo } from 'react';\n\nexport function Sidebar() {\n // Get merged menu from all plugins\n const pluginMenu = usePluginMenu();\n\n // Load user preferences (favorites, recent)\n const favorites = useFavorites();\n const recent = useRecent();\n const [collapsedGroups, setCollapsedGroups] = useState>({});\n\n // Group plugin menu by module\n const groupedMenu = useMemo(() => {\n const groups: Record = {};\n\n for (const moduleGroup of pluginMenu) {\n if (moduleGroup.children) {\n groups[moduleGroup.label] = moduleGroup.children;\n }\n }\n\n return groups;\n }, [pluginMenu]);\n\n return (\n \n );\n}\n```\n\n### Step 6: Create WMS Plugin\n\n```typescript\n// apps/wms/frontend/plugin.config.ts\n\nimport { FrameworkMPlugin } from \"@framework-m/plugin-sdk\";\n\nexport default {\n name: \"wms\",\n version: \"1.0.0\",\n\n // Define WMS menu items\n menu: [\n {\n name: \"wms.warehouse\",\n label: \"Warehouse\",\n route: \"/doctypes/wms.warehouse\",\n icon: \"warehouse\",\n module: \"Inventory\",\n category: \"Masters\",\n order: 1,\n },\n {\n name: \"wms.stock_entry\",\n label: \"Stock Entry\",\n route: \"/doctypes/wms.stock_entry\",\n icon: \"package\",\n module: \"Inventory\",\n category: \"Transactions\",\n order: 2,\n },\n {\n name: \"wms.bin_location\",\n label: \"Bin Location\",\n route: \"/doctypes/wms.bin_location\",\n icon: \"map-pin\",\n module: \"Inventory\",\n category: \"Masters\",\n order: 3,\n },\n ],\n\n // Define WMS routes\n routes: [\n {\n path: \"/wms/dashboard\",\n element: () => import(\"./pages/Dashboard\"),\n },\n {\n path: \"/wms/receiving\",\n element: () => import(\"./pages/Receiving\"),\n },\n ],\n\n // Register WMS services\n services: {\n warehouseService: () => import(\"./services/WarehouseService\"),\n },\n} satisfies FrameworkMPlugin;\n```\n\n```json\n// apps/wms/frontend/package.json\n{\n \"name\": \"@my-company/wms\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"framework-m\": {\n \"plugin\": \"./dist/plugin.config.js\",\n \"type\": \"frontend-module\"\n },\n \"dependencies\": {\n \"@framework-m/desk\": \"^0.1.0\",\n \"@framework-m/plugin-sdk\": \"^0.1.0\"\n }\n}\n```\n\n### Step 7: Vite Plugin Auto-Discovery\n\n```typescript\n// libs/framework-m-vite-plugin/src/index.ts\n\nimport { Plugin } from \"vite\";\nimport { globSync } from \"glob\";\nimport path from \"path\";\nimport fs from \"fs\";\n\nexport function frameworkMPlugin(): Plugin {\n let pluginPaths: string[] = [];\n\n return {\n name: \"framework-m-plugin\",\n\n // Discover plugins before build starts\n async buildStart() {\n // Find all workspace packages with framework-m metadata\n const packageJsonFiles = globSync(\"**/package.json\", {\n ignore: [\"**/node_modules/**\", \"**/dist/**\"],\n });\n\n pluginPaths = [];\n\n for (const pkgPath of packageJsonFiles) {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\"));\n\n // Check for framework-m plugin metadata\n if (pkg[\"framework-m\"]?.plugin) {\n const pluginConfigPath = path.resolve(\n path.dirname(pkgPath),\n pkg[\"framework-m\"].plugin,\n );\n pluginPaths.push(pluginConfigPath);\n }\n }\n\n console.log(`\u2713 Discovered ${pluginPaths.length} Framework M plugins`);\n },\n\n // Generate virtual module with all plugins\n resolveId(id) {\n if (id === \"virtual:framework-m-plugins\") {\n return \"\\0virtual:framework-m-plugins\";\n }\n },\n\n load(id) {\n if (id === \"\\0virtual:framework-m-plugins\") {\n // Generate import statements for all plugins\n const imports = pluginPaths\n .map((p, i) => `import plugin${i} from '${p}';`)\n .join(\"\\n\");\n\n const exports = `export default [${pluginPaths.map((_, i) => `plugin${i}`).join(\", \")}];`;\n\n return `${imports}\\n${exports}`;\n }\n },\n };\n}\n```\n\n### Step 8: Shell App Bootstrap\n\n```typescript\n// frontend/src/App.tsx\n\nimport { PluginRegistry, PluginRegistryProvider } from '@framework-m/plugin-sdk';\nimport { useEffect, useState } from 'react';\n// @ts-ignore - Virtual module generated by Vite plugin\nimport discoveredPlugins from 'virtual:framework-m-plugins';\n\nasync function bootstrap() {\n const registry = new PluginRegistry();\n\n // Register all discovered plugins\n for (const plugin of discoveredPlugins) {\n await registry.register(plugin);\n }\n\n return registry;\n}\n\nexport function App() {\n const [registry, setRegistry] = useState(null);\n\n useEffect(() => {\n bootstrap().then(setRegistry);\n }, []);\n\n if (!registry) {\n return ;\n }\n\n return (\n \n \n \n }>\n {/* Routes from plugins */}\n {registry.getRoutes().map((route, i) => (\n \n ))}\n \n \n \n \n );\n}\n```\n\n---\n\n## Step-by-Step Implementation Plan\n\n### Phase 1: Create @framework-m/plugin-sdk (Week 1-2)\n\n**Tasks:**\n\n1. \u2705 Create package structure\n\n ```bash\n mkdir -p libs/framework-m-plugin-sdk/src/{types,core,hooks,context}\n cd libs/framework-m-plugin-sdk\n pnpm init\n ```\n\n2. \u2705 Define types (`src/types/plugin.ts`)\n - `MenuItem` interface\n - `FrameworkMPlugin` interface\n - `ServiceFactory` type\n - `RouteObject` extensions\n\n3. \u2705 Implement `PluginRegistry` class (`src/core/PluginRegistry.ts`)\n - `register()` method\n - `getMenu()` with merging logic\n - `getRoutes()` aggregation\n - `getAllPlugins()` accessor\n\n4. \u2705 Create React hooks (`src/hooks/`)\n - `usePluginMenu()`\n - `usePlugin(name)`\n - `useService(name)`\n\n5. \u2705 Create context provider (`src/context/PluginRegistryContext.tsx`)\n\n6. \u2705 Write tests (100% coverage target)\n\n7. \u2705 Setup build (TypeScript + Vite)\n\n8. \u2705 Publish to GitLab npm registry\n\n**Deliverables:**\n\n- `@framework-m/plugin-sdk` package published\n- Full API documentation\n- Unit tests passing\n\n### Phase 2: Create @framework-m/vite-plugin (Week 2-3)\n\n**Tasks:**\n\n1. \u2705 Create package structure\n2. \u2705 Implement plugin discovery logic\n3. \u2705 Generate virtual module\n4. \u2705 Configure code-splitting\n5. \u2705 Setup HMR support\n6. \u2705 Write integration tests\n7. \u2705 Publish to GitLab npm registry\n\n**Deliverables:**\n\n- `@framework-m/vite-plugin` package published\n- Working auto-discovery\n- Documentation with examples\n\n### Phase 3: Update Shell App (Week 3-4)\n\n**Tasks:**\n\n1. \u2705 Install plugin-sdk and vite-plugin\n\n ```bash\n cd frontend\n pnpm add @framework-m/plugin-sdk @framework-m/vite-plugin\n ```\n\n2. \u2705 Update `vite.config.ts`\n\n ```typescript\n import { frameworkMPlugin } from \"@framework-m/vite-plugin\";\n\n export default defineConfig({\n plugins: [react(), frameworkMPlugin()],\n });\n ```\n\n3. \u2705 Bootstrap in `App.tsx`\n4. \u2705 Update `Sidebar.tsx` to use `usePluginMenu()`\n5. \u2705 Test with existing menu data\n6. \u2705 Add loading states and error boundaries\n\n**Deliverables:**\n\n- Shell app using plugin system\n- Sidebar consuming plugin menus\n- No breaking changes to existing functionality\n\n### Phase 4: Create Example Plugin (Week 4-5)\n\n**Tasks:**\n\n1. \u2705 Scaffold WMS plugin structure\n\n ```bash\n m new:app wms --with-frontend\n ```\n\n2. \u2705 Define `plugin.config.ts`\n3. \u2705 Add menu items\n4. \u2705 Create sample routes\n5. \u2705 Test auto-discovery\n6. \u2705 Document plugin development workflow\n\n**Deliverables:**\n\n- Working WMS plugin example\n- Plugin development guide\n- Tutorial documentation\n\n### Phase 5: CLI Enhancements (Week 5-6)\n\n**Tasks:**\n\n1. \u2705 Add `--with-frontend` flag to `m new:app`\n2. \u2705 Create plugin template\n3. \u2705 Auto-configure workspace\n4. \u2705 Update CLI documentation\n\n**Deliverables:**\n\n- Enhanced CLI command\n- Plugin scaffolding template\n- Developer workflow documentation\n\n---\n\n## Developer Workflow Examples\n\n### Single App (No Plugins) - Current Workflow\n\n```bash\n# Create project\nm new:project erp\ncd erp\n\n# Work in monolithic frontend\ncd frontend/\npnpm dev\n\n# Add custom route manually\n# Edit: frontend/src/App.tsx\n# Edit: frontend/src/pages/CustomPage.tsx\n```\n\n**No changes needed** - Existing workflow still works!\n\n### Multi-App (With Plugins) - New Workflow\n\n```bash\n# Create project\nm new:project erp\ncd erp\n\n# Create first app with frontend\nm new:app wms --with-frontend\n\n# Plugin structure auto-generated:\n# apps/wms/frontend/\n# \u251c\u2500\u2500 plugin.config.ts\n# \u251c\u2500\u2500 package.json (with framework-m metadata)\n# \u2514\u2500\u2500 src/\n# \u251c\u2500\u2500 pages/\n# \u251c\u2500\u2500 components/\n# \u2514\u2500\u2500 services/\n\n# Develop WMS plugin\ncd apps/wms/frontend\n# Edit: plugin.config.ts (add menu items, routes)\n# Edit: src/pages/Dashboard.tsx (create pages)\n\n# Run from shell\ncd ../../../frontend\npnpm dev\n# \u2713 WMS plugin auto-discovered\n# \u2713 WMS routes registered\n# \u2713 WMS menu items in sidebar\n\n# Add second app\nm new:app personnel --with-frontend\ncd frontend\npnpm dev\n# \u2713 Both WMS and Personnel plugins loaded\n# \u2713 Menus merged automatically\n```\n\n### Production Build\n\n```bash\ncd frontend\npnpm build\n\n# Output: frontend/dist/\n# index.html\n# assets/\n# main-abc123.js (Shell + framework)\n# wms-def456.js (WMS plugin - lazy loaded)\n# personnel-ghi789.js (Personnel plugin - lazy loaded)\n\n# Deploy\nm build # Bundles dist/ into Python package\npip install dist/framework_m-1.0.0-py3-none-any.whl\nm prod # Serves bundled frontend\n```\n\n---\n\n## Summary\n\n### Why SDK?\n\n| Without SDK | With SDK |\n| ------------------------- | ---------------- |\n| Manual imports everywhere | Auto-discovery |\n| Code duplication | Reusable plugins |\n| Merge conflicts | Clean separation |\n| Tight coupling | Loose coupling |\n| Hard to scale | Scales easily |\n\n### Package Roles Quick Reference\n\n| Package | Role | Published? | Used By |\n| -------------------------- | ---------------- | ------------------ | --------------- |\n| `@framework-m/desk` | Core UI library | \u2705 npm | Everyone |\n| `@framework-m/plugin-sdk` | Plugin runtime | \u2705 npm | Shell + Plugins |\n| `@framework-m/vite-plugin` | Build tooling | \u2705 npm | Shell only |\n| Shell (`frontend/`) | Composition root | \u274c Build output | - |\n| App Plugins | Domain modules | \u274c Workspace-local | Shell (bundled) |\n\n### Implementation Path\n\n1. **Phase 1** - Build `@framework-m/plugin-sdk` \u2190 **START HERE**\n2. **Phase 2** - Build `@framework-m/vite-plugin`\n3. **Phase 3** - Update shell app to use plugins\n4. **Phase 4** - Create example WMS plugin\n5. **Phase 5** - Enhance CLI for plugin scaffolding\n\n**Next Step:** Start implementing Phase 1 (plugin-sdk package)\n", "metadata": {"path": "docs/developer/plugin-architecture-implementation.md"}} {"id": "file-docs-developer-i18n.md", "type": "doc", "title": "Document: docs/developer/i18n.md", "content": "---\ntitle: Internationalization\nsidebar_position: 7\n---\n\n# Internationalization (i18n)\n\nFramework M provides built-in internationalization support through the `I18nProtocol`.\n\n## I18nProtocol\n\nThe core interface for translations:\n\n```python\nfrom framework_m.core.interfaces.i18n import I18nProtocol\n\nclass I18nProtocol(Protocol):\n async def translate(\n self,\n text: str,\n locale: str | None = None,\n *,\n context: dict[str, str] | None = None,\n default: str | None = None,\n ) -> str: ...\n\n async def get_locale(self) -> str: ...\n async def set_locale(self, locale: str) -> None: ...\n async def get_available_locales(self) -> list[str]: ...\n```\n\n## Using Translations\n\n```python\n# In a controller or service\nasync def greet_user(i18n: I18nProtocol, name: str, locale: str = \"en\"):\n greeting = await i18n.translate(\n \"Hello, {name}!\",\n locale=locale,\n context={\"name\": name}\n )\n return greeting\n```\n\n## InMemoryI18nAdapter\n\nA simple adapter for testing and development:\n\n```python\nfrom framework_m.adapters.i18n import InMemoryI18nAdapter\n\ni18n = InMemoryI18nAdapter(default_locale=\"en\")\n\n# Add translations\ni18n.add_translation(\"es\", \"Hello\", \"Hola\")\ni18n.add_translation(\"fr\", \"Hello\", \"Bonjour\")\n\n# Use translations\ntext = await i18n.translate(\"Hello\", locale=\"es\")\n# Returns: \"Hola\"\n\n# With interpolation\ni18n.add_translation(\"es\", \"Hello, {name}!\", \"\u00a1Hola, {name}!\")\ntext = await i18n.translate(\n \"Hello, {name}!\",\n locale=\"es\",\n context={\"name\": \"Juan\"}\n)\n# Returns: \"\u00a1Hola, Juan!\"\n```\n\n## Locale Resolution\n\nThe framework supports locale resolution in this order:\n\n1. **Request locale** - `Accept-Language` header\n2. **User preference** - `user.locale` setting\n3. **Tenant default** - `tenant.default_locale`\n4. **System default** - `settings.DEFAULT_LOCALE`\n\n## Creating Custom Adapters\n\nImplement `I18nProtocol` for custom translation backends:\n\n```python\nclass DatabaseI18nAdapter:\n \"\"\"Load translations from database.\"\"\"\n\n def __init__(self, repo: TranslationRepository):\n self.repo = repo\n self._cache: dict[str, dict[str, str]] = {}\n\n async def translate(\n self,\n text: str,\n locale: str | None = None,\n *,\n context: dict[str, str] | None = None,\n default: str | None = None,\n ) -> str:\n target_locale = locale or await self.get_locale()\n\n # Check cache first\n if target_locale in self._cache:\n if text in self._cache[target_locale]:\n translation = self._cache[target_locale][text]\n if context:\n for key, value in context.items():\n translation = translation.replace(f\"{{{key}}}\", value)\n return translation\n\n # Fallback to original text\n return default or text\n\n async def load_translations(self, locale: str) -> None:\n \"\"\"Load all translations for a locale into cache.\"\"\"\n translations = await self.repo.get_by_locale(locale)\n self._cache[locale] = {t.source: t.translated for t in translations}\n```\n\n## DocType Labels\n\nField labels in DocTypes support i18n:\n\n```python\nclass Invoice(BaseDocType):\n customer: str = Field(\n description=\"Customer name\", # Can be translated via i18n\n )\n```\n\nThe Meta API returns translated labels based on request locale.\n", "metadata": {"path": "docs/developer/i18n.md"}} {"id": "file-docs-developer-plugin-composition-patterns.md", "type": "doc", "title": "Document: docs/developer/plugin-composition-patterns.md", "content": "# Plugin Composition Patterns\n\n## Overview\n\nFramework M's multi-package UI architecture enables powerful composition patterns where multiple packages can contribute, extend, and override UI components at build time.\n\n## Registration Order\n\nPlugins are registered in a specific order that determines override precedence:\n\n```typescript\n// 1. Installed packages (alphabetically by package name)\nregisterPlugin({ name: \"business_m\" });\nregisterPlugin({ name: \"crm_app\" });\nregisterPlugin({ name: \"wms_app\" });\n\n// 2. Local workspace apps (alphabetically by app name)\nregisterPlugin({ name: \"custom_app\" });\n\n// Later registrations win for the same route/component\n```\n\n## Pattern 1: Page Override\n\nReplace a page from another package entirely.\n\n### Example: Custom CRM Lead Form\n\n**Standard CRM (crm_app):**\n\n```typescript\n// crm_app/frontend/index.ts\nregisterPlugin({\n name: \"crm_app\",\n pages: [\n {\n path: \"/app/lead/:id\",\n component: LeadForm,\n },\n ],\n});\n```\n\n**Custom Extension (acme_crm_ext):**\n\n```typescript\n// acme_crm_ext/frontend/index.ts\nimport { LeadForm as StandardLeadForm } from \"@crm-app/frontend/pages/LeadForm\";\n\nfunction AcmeLeadForm(props) {\n return (\n
\n \n {/* Add Acme-specific fields */}\n \n
\n );\n}\n\nregisterPlugin({\n name: \"acme_crm_ext\",\n pages: [\n {\n path: \"/app/lead/:id\",\n component: AcmeLeadForm,\n override: true, // \u2190 Explicitly override\n },\n ],\n});\n```\n\n**Result:** When users navigate to `/app/lead/123`, they see `AcmeLeadForm` instead of the standard `LeadForm`.\n\n## Pattern 2: Slot Injection\n\nAdd components to predefined slots without overriding the entire page.\n\n### Example: Dashboard Widgets\n\n**Base Package (framework-m-desk):**\n\n```typescript\n// Defines slot in Dashboard\nregisterPlugin({\n name: \"framework_m_desk\",\n pages: [\n {\n path: \"/app/dashboard\",\n component: Dashboard,\n slots: [\"dashboard.widgets\"], // \u2190 Define available slots\n },\n ],\n});\n\n// Dashboard.tsx renders slot\nimport { SlotRenderer } from \"@framework-m/core\";\n\nfunction Dashboard() {\n return (\n
\n

Dashboard

\n \n
\n );\n}\n```\n\n**Extension Packages (multiple):**\n\n```typescript\n// business_m/frontend/index.ts\nregisterPlugin({\n name: \"business_m\",\n slots: [\n {\n slot: \"dashboard.widgets\",\n component: SalesWidget,\n priority: 10,\n },\n ],\n});\n\n// wms_app/frontend/index.ts\nregisterPlugin({\n name: \"wms_app\",\n slots: [\n {\n slot: \"dashboard.widgets\",\n component: StockLevelWidget,\n priority: 20, // \u2190 Higher priority = rendered first\n },\n ],\n});\n\n// crm_app/frontend/index.ts\nregisterPlugin({\n name: \"crm_app\",\n slots: [\n {\n slot: \"dashboard.widgets\",\n component: LeadPipelineWidget,\n priority: 15,\n },\n ],\n});\n```\n\n**Result:** Dashboard automatically renders all three widgets in priority order: `StockLevelWidget` (20) \u2192 `LeadPipelineWidget` (15) \u2192 `SalesWidget` (10).\n\n## Pattern 3: Component Extension\n\nWrap or enhance components from base packages.\n\n### Example: Enhanced Customer Form\n\n**Base Package (business_m):**\n\n```typescript\n// business_m/frontend/components/CustomerForm.tsx\nexport function CustomerForm({ customer, onSave }) {\n return (\n
\n \n \n \n
\n );\n}\n```\n\n**Extension (custom_app):**\n\n```typescript\n// custom_app/frontend/components/EnhancedCustomerForm.tsx\nimport { CustomerForm } from \"@business-m/frontend/components/CustomerForm\";\nimport { CreditScoreWidget } from \"./CreditScoreWidget\";\n\nexport function EnhancedCustomerForm(props) {\n return (\n
\n {/* Standard form */}\n \n\n {/* Additional features */}\n \n \n
\n );\n}\n\nregisterPlugin({\n name: \"custom_app\",\n components: {\n CustomerForm: EnhancedCustomerForm, // \u2190 Override component\n },\n});\n```\n\n## Pattern 4: Hook Composition\n\nExtend or wrap hooks from other packages.\n\n### Example: Enhanced Data Fetching\n\n**Base Package:**\n\n```typescript\n// business_m/frontend/hooks/useCustomer.ts\nexport function useCustomer(id: string) {\n const { data, isLoading } = useQuery([\"customer\", id], () =>\n fetch(`/api/customer/${id}`),\n );\n\n return { customer: data, isLoading };\n}\n```\n\n**Extension:**\n\n```typescript\n// custom_app/frontend/hooks/useCustomerWithScore.ts\nimport { useCustomer } from \"@business-m/frontend/hooks/useCustomer\";\n\nexport function useCustomerWithScore(id: string) {\n const { customer, isLoading } = useCustomer(id);\n\n // Add credit score data\n const { data: score } = useQuery([\"credit-score\", id], () =>\n fetch(`/api/credit-score/${id}`),\n );\n\n return {\n customer: { ...customer, creditScore: score },\n isLoading,\n };\n}\n```\n\n## Pattern 5: Multi-Package Composition\n\nCombine features from multiple packages into a unified experience.\n\n### Example: Unified Customer View\n\n```typescript\n// custom_app/frontend/pages/UnifiedCustomer.tsx\nimport { CustomerCard } from \"@business-m/frontend/components/CustomerCard\";\nimport { CustomerOrders } from \"@wms-app/frontend/components/CustomerOrders\";\nimport { CustomerLeads } from \"@crm-app/frontend/components/CustomerLeads\";\nimport { CustomerSupport } from \"@hr-app/frontend/components/CustomerSupport\";\n\nexport function UnifiedCustomerView({ customerId }) {\n return (\n \n \n {/* From business-m */}\n \n\n {/* From wms-app */}\n \n\n {/* From crm-app */}\n \n\n {/* From hr-app */}\n \n \n \n );\n}\n\nregisterPlugin({\n name: \"custom_app\",\n pages: [\n {\n path: \"/app/customer/:id/unified\",\n component: UnifiedCustomerView,\n },\n ],\n});\n```\n\n## Pattern 6: Conditional Registration\n\nRegister different UI based on configuration or feature flags.\n\n### Example: Environment-Specific Features\n\n```typescript\n// custom_app/frontend/index.ts\nimport { registerPlugin } from \"@framework-m/core\";\nimport { DevelopmentTools } from \"./components/DevelopmentTools\";\nimport { AdminPanel } from \"./components/AdminPanel\";\n\nconst isDevelopment = import.meta.env.MODE === \"development\";\nconst enableAdmin = import.meta.env.VITE_ENABLE_ADMIN === \"true\";\n\nregisterPlugin({\n name: \"custom_app\",\n\n slots: [\n // Development-only tools\n ...(isDevelopment\n ? [\n {\n slot: \"desk.toolbar\",\n component: DevelopmentTools,\n },\n ]\n : []),\n\n // Admin-only panel\n ...(enableAdmin\n ? [\n {\n slot: \"desk.sidebar.top\",\n component: AdminPanel,\n },\n ]\n : []),\n ],\n});\n```\n\n## Pattern 7: Type-Safe Component Registry\n\nShare component types across packages for type-safe composition.\n\n### Setup Type Definitions\n\n**Base Package (business_m):**\n\n```typescript\n// business_m/frontend/types/components.ts\nexport interface CustomerCardProps {\n customerId: string;\n variant?: \"compact\" | \"detailed\";\n onUpdate?: (customer: Customer) => void;\n}\n\n// business_m/frontend/components/CustomerCard.tsx\nimport type { CustomerCardProps } from \"../types/components\";\n\nexport function CustomerCard(props: CustomerCardProps) {\n // Implementation\n}\n```\n\n**Extension Package:**\n\n```typescript\n// custom_app/frontend/components/DashboardCustomerCard.tsx\nimport type { CustomerCardProps } from \"@business-m/frontend/types/components\";\nimport { CustomerCard } from \"@business-m/frontend/components/CustomerCard\";\n\n// Type-safe wrapper\nexport function DashboardCustomerCard(props: CustomerCardProps) {\n return (\n
\n \n
\n );\n}\n```\n\n## Pattern 8: Theme Customization\n\nOverride theme tokens from base packages.\n\n### Example: Custom Branding\n\n**Base Theme (framework-m-desk):**\n\n```typescript\n// framework-m-desk/frontend/theme/index.ts\nexport const theme = {\n colors: {\n primary: \"#3b82f6\",\n secondary: \"#8b5cf6\",\n },\n fonts: {\n body: \"Inter, sans-serif\",\n heading: \"Poppins, sans-serif\",\n },\n};\n```\n\n**Custom Theme (custom_app):**\n\n```typescript\n// custom_app/frontend/theme/index.ts\nimport { theme as baseTheme } from \"@framework-m-desk/frontend/theme\";\n\nexport const theme = {\n ...baseTheme,\n colors: {\n ...baseTheme.colors,\n primary: \"#0ea5e9\", // \u2190 Acme Corp blue\n secondary: \"#06b6d4\",\n },\n fonts: {\n ...baseTheme.fonts,\n body: \"Roboto, sans-serif\", // \u2190 Custom font\n },\n};\n\nregisterPlugin({\n name: \"custom_app\",\n theme, // \u2190 Override theme\n});\n```\n\n## Override Resolution Rules\n\nWhen multiple plugins register the same path/component/slot:\n\n### Pages\n\n```typescript\n// Rule: Last registration wins\n// crm_app registers first\nregisterPlugin({\n name: \"crm_app\",\n pages: [{ path: \"/app/lead/:id\", component: LeadForm }],\n});\n\n// custom_app registers later \u2192 WINS\nregisterPlugin({\n name: \"custom_app\",\n pages: [{ path: \"/app/lead/:id\", component: CustomLeadForm }],\n});\n\n// Result: /app/lead/123 renders CustomLeadForm\n```\n\n### Slots\n\n```typescript\n// Rule: All registrations render (sorted by priority)\nregisterPlugin({\n name: \"pkg1\",\n slots: [{ slot: \"sidebar\", component: Widget1, priority: 10 }],\n});\n\nregisterPlugin({\n name: \"pkg2\",\n slots: [{ slot: \"sidebar\", component: Widget2, priority: 20 }],\n});\n\n// Result: Renders [Widget2, Widget1] (priority 20 first)\n```\n\n### Components\n\n```typescript\n// Rule: Last registration in component registry wins\nregisterPlugin({\n name: \"business_m\",\n components: { CustomerCard: BaseCustomerCard },\n});\n\nregisterPlugin({\n name: \"custom_app\",\n components: { CustomerCard: CustomCustomerCard }, // \u2190 WINS\n});\n\n// Other plugins importing CustomerCard get CustomCustomerCard\n```\n\n## Best Practices\n\n### 1. Explicit Override Flag\n\nAlways use `override: true` when intentionally replacing a page:\n\n```typescript\nregisterPlugin({\n name: \"custom_app\",\n pages: [\n {\n path: \"/app/customer/:id\",\n component: CustomCustomerPage,\n override: true, // \u2190 Makes intent clear\n },\n ],\n});\n```\n\n### 2. Semantic Slot Names\n\nUse hierarchical, descriptive slot names:\n\n```typescript\n// \u2705 Good\n\"desk.sidebar.top\";\n\"desk.sidebar.bottom\";\n\"page.customer.header\";\n\"page.customer.footer\";\n\n// \u274c Bad\n\"slot1\";\n\"sidebar\";\n\"custom\";\n```\n\n### 3. Document Public APIs\n\nIf your package exports components for others to use, document them:\n\n````typescript\n/**\n * Customer card component with customization options.\n *\n * @example\n * ```tsx\n * import { CustomerCard } from \"@business-m/frontend\";\n *\n * \n * ```\n */\nexport function CustomerCard(props: CustomerCardProps) {\n // Implementation\n}\n````\n\n### 4. Version Compatibility\n\nCheck for compatible versions when composing:\n\n```typescript\nimport { version as businessMVersion } from \"@business-m/frontend\";\n\nif (!businessMVersion.startsWith(\"1.\")) {\n console.warn(\"custom_app requires business-m ^1.0.0\");\n}\n```\n\n## Debugging Composition\n\n### View Registered Plugins\n\n```typescript\nimport { getRegisteredPlugins } from \"@framework-m/core\";\n\nconsole.log(getRegisteredPlugins());\n// [\n// { name: \"business_m\", version: \"1.0.0\", pages: [...], slots: [...] },\n// { name: \"crm_app\", version: \"2.1.0\", pages: [...], slots: [...] },\n// { name: \"custom_app\", version: \"1.0.0\", pages: [...], slots: [...] },\n// ]\n```\n\n### Debug Slot Rendering\n\n```typescript\nimport { getSlotComponents } from \"@framework-m/core\";\n\nconsole.log(getSlotComponents(\"dashboard.widgets\"));\n// [\n// { plugin: \"wms_app\", component: StockLevelWidget, priority: 20 },\n// { plugin: \"crm_app\", component: LeadPipelineWidget, priority: 15 },\n// { plugin: \"business_m\", component: SalesWidget, priority: 10 },\n// ]\n```\n\n### Trace Component Origin\n\n```typescript\nimport { getComponentSource } from \"@framework-m/core\";\n\nconst source = getComponentSource(\"CustomerCard\");\nconsole.log(source);\n// { plugin: \"custom_app\", originalPlugin: \"business_m\", overridden: true }\n```\n\n## Next Steps\n\n- [Package Frontend Guide](./package-frontend-guide.md) - Complete package structure reference\n- [Migration Path](../adr/0010-multi-package-ui-composition.md#migration-path) - Migrate existing apps/ to packages\n- [Protocol Reference](../developer/generated/index.md) - Full plugin-related protocol documentation\n", "metadata": {"path": "docs/developer/plugin-composition-patterns.md"}} {"id": "file-docs-developer-desk-setup.md", "type": "doc", "title": "Document: docs/developer/desk-setup.md", "content": "---\ntitle: Desk Setup\nsidebar_position: 9\n---\n\n# Desk Setup Guide\n\n## Running Desk\n\nThe Desk provides the main user interface for managing your Framework M application data and workflows.\n\n### Single Command (Production)\n\n```bash\nm prod\n```\n\nThis starts the backend server with the bundled Desk UI on port 8888.\n\n### Development Mode\n\nFor development with hot reload:\n\n```bash\nm dev\n```\n\nThis starts both backend and frontend with automatic hot reload using honcho process manager.\n\n### URL Structure\n\nThe Desk uses a **path prefix architecture** for clean separation:\n\n- **UI**: `http://127.0.0.1:8888/desk/`\n - Main interface for managing data\n - All frontend routes are under `/desk/*`\n\n- **API**: `http://127.0.0.1:8888/api/`\n - Backend endpoints for data operations\n - All API routes are under `/api/*`\n\n- **Root**: `http://127.0.0.1:8888/`\n - Automatically redirects to `/desk/`\n\n## Commands\n\n### `m prod` - Production Mode\n\nRuns the backend with bundled static Desk UI:\n\n```bash\n# Default (port 8888)\nm prod\n\n# Custom port\nm prod --port 3000\n\n# Custom app module\nm prod --app myapp:create_app\n\n# With auto-reload\nm prod --reload\n```\n\n**Use Cases:**\n\n- Production deployments\n- Testing bundled UI\n- Single-process server\n\n### `m dev` - Development Mode\n\nRuns backend and frontend concurrently with hot reload:\n\n```bash\n# Backend + Frontend\nm dev\n\n# Backend + Frontend + Studio\nm dev --studio\n\n# Custom backend module\nm dev --app myapp:create_app\n\n# Custom frontend directory\nm dev --frontend-dir ./custom-ui\n\n# Backend only (no frontend)\nm dev --no-frontend\n\n# Custom ports\nm dev --port 9000 --frontend-port 3000 --studio-port 9999\n\n# Enable worker and scheduler\nm dev --enable-worker --enable-scheduler\n\n# Use custom Procfile\nm dev --procfile Procfile.dev\n```\n\n**Features:**\n\n- \u2705 **Hot Reload**: Backend (uvicorn) and frontend (Vite HMR)\n- \u2705 **Process Management**: Honcho manages multiple processes\n- \u2705 **Concurrent Output**: Color-coded logs with process prefixes\n- \u2705 **Graceful Shutdown**: Ctrl+C stops all processes cleanly\n- \u2705 **Configurable**: Supports Procfile and m.toml configuration\n- \u2705 **Extensible**: Add workers, schedulers, custom processes\n\n**Default Ports:**\n| Process | Port |\n|---------|------|\n| Backend | 8888 |\n| Frontend | 5173 |\n| Studio | 9999 |\n\n**New CLI Flags:**\n| Flag | Description |\n|------|-------------|\n| `--enable-worker` | Start worker process for background jobs |\n| `--enable-scheduler` | Start scheduler process for periodic tasks |\n| `--procfile ` | Load custom Procfile for process definitions |\n\n## Configuration\n\nThe `m dev` command supports multiple configuration sources with the following precedence:\n\n**Configuration Precedence (highest to lowest):**\n\n1. **CLI flags** (e.g., `--enable-worker`, `--port 9000`)\n2. **Procfile** (if specified with `--procfile`)\n3. **m.toml [dev] section**\n4. **Default processes**\n\n### Using m.toml\n\nCreate a `m.toml` file in your project root:\n\n```toml\n[dev]\n# Override default ports\nbackend_port = 8888\nfrontend_port = 5173\n\n# Enable optional processes\nenable_worker = true\nenable_scheduler = false\n\n# Override default commands (optional)\nbackend_command = \"uvicorn app:app --reload --port 8888 --log-level debug\"\n\n# Use a custom Procfile (optional)\nprocfile = \"Procfile.dev\"\n\n# Define custom processes\n[dev.processes.mailhog]\ncommand = \"mailhog -smtp-bind-addr 127.0.0.1:1025 -ui-bind-addr 127.0.0.1:8025\"\nenabled = false # Set to true to enable\n\n[dev.processes.redis]\ncommand = \"redis-server --port 6379\"\nenabled = false\n```\n\n**See full example:** [`docs/examples/m.toml`](../examples/m.toml)\n\n### Using Procfile\n\nCreate a `Procfile.dev` file in your project root:\n\n```procfile\n# Procfile.dev\nbackend: python -m uvicorn framework_m_standard.adapters.web.app:create_app --host 127.0.0.1 --port 8888 --reload\nfrontend: cd frontend && pnpm dev --port 5173\nworker: m worker --concurrency 4\nscheduler: m worker --scheduler-only\n```\n\nThen run:\n\n```bash\nm dev --procfile Procfile.dev\n```\n\n**See full example:** [`docs/examples/Procfile.dev`](../examples/Procfile.dev)\n\n### Configuration Examples\n\n**Example 1: Enable worker in m.toml**\n\n```toml\n[dev]\nenable_worker = true\n```\n\n```bash\nm dev # Worker automatically starts\n```\n\n**Example 2: Mix m.toml with CLI flags**\n\n```toml\n[dev]\nbackend_port = 9000\n```\n\n```bash\nm dev --enable-scheduler # Uses port 9000 from m.toml, adds scheduler from CLI\n```\n\n**Example 3: Full custom Procfile**\n\n```procfile\nbackend: uvicorn app:app --reload\nfrontend: cd ui && pnpm dev\nworker: m worker --concurrency 8\nscheduler: m worker --scheduler-only\nmailhog: mailhog -smtp-bind-addr 127.0.0.1:1025\nredis: redis-server\n```\n\n```bash\nm dev --procfile Procfile.dev\n```\n\n## Architecture\n\n### Path Prefix Separation\n\nThe Desk implements complete separation between UI and API routes:\n\n```\n/desk/* \u2192 React SPA (HTML pages)\n/api/* \u2192 JSON API endpoints\n/ \u2192 Redirects to /desk/\n```\n\nThis architecture:\n\n- \u2705 Prevents route collisions\n- \u2705 Enables clean URLs (no hash fragments)\n- \u2705 Works with single-command deployment\n- \u2705 Supports proper Content-Type headers\n\n### Static Assets\n\nStatic assets (JS, CSS, images) are served at:\n\n```\n/desk/assets/*\n```\n\nThe Vite build process outputs to `libs/framework-m/src/framework_m/static/` which is included in the Python package.\n\n### Process Management (m dev)\n\nWhen running `m dev`, honcho orchestrates multiple processes:\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 honcho.manager.Manager \u2502\n\u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\u2502\n\u2502 \u2502 backend \u2502 \u2502 frontend \u2502 \u2502 studio \u2502\u2502\n\u2502 \u2502 :8888 \u2502 \u2502 :5173 \u2502 \u2502 :9999 \u2502\u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\u2502\n\u2502 \u2502\n\u2502 \u2022 Color-coded output \u2502\n\u2502 \u2022 Concurrent execution \u2502\n\u2502 \u2022 Graceful shutdown \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## API Endpoints\n\n### Health Check\n\n```bash\ncurl http://127.0.0.1:8888/health\n```\n\n### Authentication\n\n```bash\ncurl http://127.0.0.1:8888/api/v1/auth/me\n```\n\n### List DocTypes\n\n```bash\ncurl http://127.0.0.1:8888/api/meta/doctypes\n```\n\n### Translations\n\n```bash\ncurl http://127.0.0.1:8888/api/v1/Translation?locale=en\n```\n\n## Building for Production\n\n### Build Frontend\n\n```bash\ncd frontend\npnpm build\n```\n\nThis outputs to `dist/` which should be copied to `libs/framework-m/src/framework_m/static/`.\n\n### Update Bundled Static\n\n```bash\n# After building frontend\ncp -r frontend/dist/* libs/framework-m/src/framework_m/static/\n```\n\n### Package Python\n\n```bash\ncd libs/framework-m\nuv build\n```\n\nThe built wheel includes the static frontend assets.\n\n## Development Workflow\n\n### Initial Setup\n\n1. **Install dependencies:**\n\n ```bash\n uv sync\n ```\n\n2. **Install your app (if custom):**\n\n ```bash\n cd apps/myapp\n uv pip install -e .\n ```\n\n3. **Start development server:**\n ```bash\n m dev\n ```\n\n### Hot Reload in Action\n\n**Backend Changes:**\n\n```bash\n# Edit any Python file\n# WatchFiles detects changes\n# Server automatically restarts\n```\n\n**Frontend Changes:**\n\n```bash\n# Edit any React/TS file\n# Vite HMR updates instantly\n# No page reload needed\n```\n\n### Git Noise and Local Builds\n\n**Avoid running `pnpm build` locally during development.**\n\nRunning `pnpm build` updates the static assets in `libs/framework-m/src/framework_m/static/`. Since Vite includes hashes in filenames (e.g., `index-a1b2c3.js`), every build modifies tracked files, creating unnecessary git noise.\n\n**Best Practice:**\n\n- Use `m dev` for frontend development\n- **Rely on CI/CD** for release builds\n- Local `pnpm build` is only necessary to verify production artifacts\n\n## Troubleshooting\n\n### UI Downloads Instead of Rendering\n\nIf the browser downloads HTML files instead of rendering them, check:\n\n1. **Content-Type Headers**: Ensure responses have `Content-Type: text/html; charset=utf-8`\n2. **Route Handlers**: Verify return type is `Response` not union types\n3. **Browser Cache**: Clear browser cache and hard reload (Ctrl+Shift+R)\n\n### 404 Errors on Deep Links\n\nIf refreshing a page like `/desk/doctypes/User` gives 404:\n\n1. **Check Route Handler**: Ensure `/desk/{path:path}` wildcard route exists\n2. **SPA Fallback**: Verify the handler returns `index.html` for non-file paths\n3. **Base Path**: Confirm React Router uses `basename=\"/desk\"`\n\n### API Calls Failing\n\nIf API calls return errors:\n\n1. **CORS**: Check CORS configuration allows your origin\n2. **Path Prefix**: Ensure API calls use `/api/*` prefix\n3. **Server Running**: Verify `m prod` or `m dev` is running on the correct port\n\n### Hot Reload Not Working\n\nIf changes aren't being detected:\n\n**Backend:**\n\n1. Check WatchFiles is enabled (look for \"Started reloader process\" in logs)\n2. Verify file is in watched directory\n3. Try `--reload` flag explicitly\n\n**Frontend:**\n\n1. Ensure `m dev` is running (not `m prod`)\n2. Check Vite dev server is on port 5173\n3. Verify WebSocket connection in browser DevTools\n\n### DocTypes Not Appearing\n\nIf your custom DocTypes don't show in the sidebar:\n\n1. **Install your app**: `cd apps/myapp && uv pip install -e .`\n2. **Check entry points**: Verify `pyproject.toml` has `[project.entry-points.\"framework_m.apps\"]`\n3. **Restart server**: DocTypes are discovered at startup\n4. **Check logs**: Look for \"Discovered X DocType(s) from app 'myapp'\"\n\n## Quick Reference\n\n| Command | Use Case | Hot Reload |\n| --------------------- | ------------------------- | ------------------- |\n| `m prod` | Production, bundled UI | Optional (--reload) |\n| `m dev` | Development, both servers | \u2705 Always |\n| `m dev --studio` | Development + Studio | \u2705 Always |\n| `m dev --no-frontend` | Backend only dev | \u2705 Backend only |\n", "metadata": {"path": "docs/developer/desk-setup.md"}} {"id": "file-docs-developer-tenancy-modes.md", "type": "doc", "title": "Document: docs/developer/tenancy-modes.md", "content": "---\ntitle: Tenancy Modes\nsidebar_position: 6\n---\n\n# Multi-Tenancy: Architecture and Usage Guide\n\nThis guide covers Framework M's multi-tenancy architecture, including the two tenancy modes (Single and Multi-Tenant), tenant isolation patterns, and integration patterns for both backend and frontend.\n\n---\n\n## Table of Contents\n\n1. [Overview](#overview)\n2. [Mode A: Single Tenant (Indie)](#mode-a-single-tenant-indie)\n3. [Mode B: Multi-Tenant (Enterprise)](#mode-b-multi-tenant-enterprise)\n4. [Tenant Isolation Patterns](#tenant-isolation-patterns)\n5. [Using TenantContext in Code](#using-tenantcontext-in-code)\n6. [Frontend Multi-Tenancy](#frontend-multi-tenancy)\n7. [Switching Adapters](#switching-adapters)\n\n---\n\n## Overview\n\nFramework M provides a flexible multi-tenancy system with two operational modes:\n\n- **Mode A (Single Tenant / Indie)**: Uses `ImplicitTenantAdapter` with a fixed \"default\" tenant. No database lookups, perfect for standalone deployments.\n- **Mode B (Multi-Tenant / Enterprise)**: Uses `HeaderTenantAdapter` to extract tenant information from gateway headers. Requires an API gateway (e.g., Keycloak, Auth0, Citadel IAM) for tenant resolution.\n\nBoth modes implement the `TenantProtocol` interface, allowing seamless switching between deployment configurations.\n\n### TenantProtocol Interface\n\n```python\nfrom typing import Protocol, Any\n\nclass TenantProtocol(Protocol):\n \"\"\"Protocol for tenant resolution and attribute retrieval.\"\"\"\n\n def get_current_tenant(self) -> str:\n \"\"\"Get the current tenant ID from request context.\"\"\"\n ...\n\n def get_tenant_attributes(self, tenant_id: str) -> dict[str, Any]:\n \"\"\"Get attributes for a specific tenant.\"\"\"\n ...\n```\n\n### TenantContext\n\nAll tenant adapters return a `TenantContext` object:\n\n```python\nfrom framework_m.core.interfaces.tenant import TenantContext\n\nctx = TenantContext(\n tenant_id=\"acme-corp\",\n attributes={\n \"plan\": \"enterprise\",\n \"features\": [\"advanced_reports\", \"api_access\"],\n \"max_users\": 100\n },\n is_default=False\n)\n```\n\n---\n\n## Mode A: Single Tenant (Indie)\n\n### Overview\n\n`ImplicitTenantAdapter` is designed for standalone deployments where you have a single tenant. It provides:\n\n- **No database lookups**: Tenant is hardcoded or configured via `framework_config.toml`\n- **Unlimited features**: Default attributes give full access to all features\n- **Zero overhead**: No runtime tenant resolution cost\n- **Simple deployment**: Perfect for indie apps, MVPs, and single-customer deployments\n\n### Configuration\n\nConfigure single-tenant mode in `framework_config.toml`:\n\n```toml\n[tenancy]\nmode = \"single\"\ndefault_tenant_id = \"default\" # Or your organization name\n```\n\n### Usage\n\n#### Basic Usage\n\n```python\nfrom framework_m.adapters.tenant import ImplicitTenantAdapter\n\n# Use default configuration\nadapter = ImplicitTenantAdapter()\ntenant_id = adapter.get_current_tenant() # \"default\"\nattrs = adapter.get_tenant_attributes(\"default\")\n# {\"plan\": \"unlimited\", \"features\": \"*\"}\n```\n\n#### Custom Tenant ID\n\n```python\n# Override tenant ID (useful for branding)\nadapter = ImplicitTenantAdapter(tenant_id=\"acme-corp\")\ntenant_id = adapter.get_current_tenant() # \"acme-corp\"\n```\n\n#### Custom Attributes\n\n```python\n# Override default attributes\nadapter = ImplicitTenantAdapter(\n tenant_id=\"my-app\",\n attributes={\n \"plan\": \"pro\",\n \"features\": [\"reports\", \"api\", \"webhooks\"],\n \"max_storage_gb\": 100\n }\n)\n```\n\n#### Getting Full Context\n\n```python\nadapter = ImplicitTenantAdapter()\nctx = adapter.get_context()\n\nprint(ctx.tenant_id) # \"default\"\nprint(ctx.attributes) # {\"plan\": \"unlimited\", \"features\": \"*\"}\nprint(ctx.is_default) # True\n```\n\n### When to Use Mode A\n\n\u2705 **Use ImplicitTenantAdapter when:**\n\n- Building an indie app or MVP\n- Single customer deployment\n- No need for tenant isolation\n- Want to minimize complexity\n- Running on-premise for one organization\n- Prototype or demo environments\n\n\u274c **Don't use when:**\n\n- You need to serve multiple customers\n- Tenant isolation is required\n- You plan to scale to multi-tenancy later (start with Mode B)\n\n### Implementation Details\n\n```python\n# frameworks-m/src/framework_m/adapters/tenant.py\nclass ImplicitTenantAdapter:\n \"\"\"Single-tenant mode adapter.\"\"\"\n\n def __init__(\n self,\n tenant_id: str | None = None,\n attributes: dict[str, Any] | None = None,\n ) -> None:\n self._tenant_id = tenant_id or get_default_tenant_id()\n self._attributes = attributes or {\n \"plan\": \"unlimited\",\n \"features\": \"*\",\n }\n\n def get_current_tenant(self) -> str:\n return self._tenant_id\n\n def get_tenant_attributes(self, tenant_id: str) -> dict[str, Any]:\n return self._attributes.copy()\n\n def get_context(self) -> TenantContext:\n return TenantContext(\n tenant_id=self._tenant_id,\n attributes=self._attributes.copy(),\n is_default=True,\n )\n```\n\n---\n\n## Mode B: Multi-Tenant (Enterprise)\n\n### Overview\n\n`HeaderTenantAdapter` is designed for multi-tenant SaaS deployments where an API gateway handles authentication and populates tenant information in HTTP headers.\n\n**Key Features:**\n\n- **Gateway integration**: Works with Keycloak, Auth0, AWS ALB, Citadel IAM\n- **Zero trust**: Framework never authenticates users, only reads headers\n- **Feature flags**: Per-tenant attributes control feature availability\n- **Dynamic attributes**: Plan, features, limits passed in headers\n- **Security**: Gateway ensures headers can't be spoofed\n\n### Architecture\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Browser \u2502\u2500\u2500\u2500\u2500\u2500\u25b6\u2502 API Gateway \u2502\u2500\u2500\u2500\u2500\u2500\u25b6\u2502 Framework M \u2502\n\u2502 \u2502 \u2502 (Keycloak) \u2502 \u2502 (Backend) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u251c\u2500 Authenticate user\n \u251c\u2500 Resolve tenant from JWT\n \u251c\u2500 Fetch tenant attributes\n \u2514\u2500 Add headers:\n - X-Tenant-ID: acme-corp\n - X-Tenant-Attributes: {...}\n```\n\n### Configuration\n\nConfigure multi-tenant mode in `framework_config.toml`:\n\n```toml\n[tenancy]\nmode = \"multi\"\ndefault_tenant_id = \"default\" # Fallback if header missing\n```\n\n### Gateway Setup\n\n#### Expected Headers\n\nThe gateway must populate these headers:\n\n```http\nX-Tenant-ID: acme-corp\nX-Tenant-Attributes: {\"plan\": \"enterprise\", \"features\": [\"reports\", \"api\"], \"max_users\": 100}\n```\n\n#### Example: Keycloak Configuration\n\n1. **Add Tenant Claim to JWT:**\n\n```json\n// Keycloak Mapper: Add \"tenant_id\" claim\n{\n \"tenant_id\": \"acme-corp\",\n \"tenant_attributes\": {\n \"plan\": \"enterprise\",\n \"features\": [\"advanced_reports\", \"api_access\"],\n \"max_users\": 100\n }\n}\n```\n\n2. **Configure Gateway to Extract Headers:**\n\n```nginx\n# Nginx config (or use Keycloak Gatekeeper/Louketo)\nlocation /api/ {\n proxy_set_header X-Tenant-ID $jwt_claim_tenant_id;\n proxy_set_header X-Tenant-Attributes $jwt_claim_tenant_attributes;\n proxy_pass http://framework-m-backend:8000;\n}\n```\n\n#### Example: Citadel IAM Integration\n\nCitadel IAM can be configured to automatically populate tenant headers:\n\n```yaml\n# citadel-config.yaml\ntenant_resolution:\n source: jwt_claim\n claim_name: tenant_id\n attributes_claim: tenant_attributes\n headers:\n tenant_id: X-Tenant-ID\n attributes: X-Tenant-Attributes\n```\n\n### Usage\n\n#### Basic Usage\n\n```python\nfrom framework_m.adapters.tenant import HeaderTenantAdapter\n\n# In a Litestar dependency provider\nheaders = {\n \"x-tenant-id\": \"acme-corp\",\n \"x-tenant-attributes\": '{\"plan\": \"enterprise\", \"features\": [\"reports\"]}',\n}\n\nadapter = HeaderTenantAdapter(headers=headers)\ntenant_id = adapter.get_current_tenant() # \"acme-corp\"\n```\n\n#### Custom Header Names\n\n```python\n# Use custom header names (e.g., for legacy systems)\nadapter = HeaderTenantAdapter(\n headers=headers,\n tenant_header=\"x-organization-id\",\n attributes_header=\"x-organization-data\"\n)\n```\n\n#### Fallback Behavior\n\n```python\n# If X-Tenant-ID header is missing, falls back to default\nheaders = {} # No tenant header\nadapter = HeaderTenantAdapter(headers=headers)\ntenant_id = adapter.get_current_tenant() # \"default\"\n```\n\n#### Getting Full Context\n\n```python\nheaders = {\n \"x-tenant-id\": \"acme-corp\",\n \"x-tenant-attributes\": '{\"plan\": \"enterprise\", \"max_users\": 100}',\n}\n\nadapter = HeaderTenantAdapter(headers=headers)\nctx = adapter.get_context()\n\nprint(ctx.tenant_id) # \"acme-corp\"\nprint(ctx.attributes) # {\"plan\": \"enterprise\", \"max_users\": 100}\nprint(ctx.is_default) # False\n```\n\n### When to Use Mode B\n\n\u2705 **Use HeaderTenantAdapter when:**\n\n- Building a multi-tenant SaaS product\n- Serving multiple customers from one deployment\n- Need tenant data isolation\n- Using an API gateway (Keycloak, Auth0, etc.)\n- Require feature flags per tenant\n- Different plans/tiers for customers\n\n\u274c **Don't use when:**\n\n- Single customer deployment\n- No API gateway infrastructure\n- Can't guarantee header security\n\n### Implementation Details\n\n```python\n# framework-m/src/framework_m/adapters/tenant.py\nclass HeaderTenantAdapter:\n \"\"\"Multi-tenant mode adapter.\"\"\"\n\n def __init__(\n self,\n headers: Mapping[str, str],\n tenant_header: str = \"x-tenant-id\",\n attributes_header: str = \"x-tenant-attributes\",\n default_tenant_id: str = \"default\",\n ) -> None:\n self._headers = headers\n self._tenant_header = tenant_header\n self._attributes_header = attributes_header\n self._default_tenant_id = default_tenant_id\n\n def get_current_tenant(self) -> str:\n return self._headers.get(self._tenant_header, self._default_tenant_id)\n\n def get_tenant_attributes(self, tenant_id: str) -> dict[str, Any]:\n attrs_json = self._headers.get(self._attributes_header, \"\")\n if not attrs_json:\n return {}\n\n try:\n result = json.loads(attrs_json)\n return result if isinstance(result, dict) else {}\n except json.JSONDecodeError:\n return {}\n\n def get_context(self) -> TenantContext:\n tenant_id = self.get_current_tenant()\n return TenantContext(\n tenant_id=tenant_id,\n attributes=self.get_tenant_attributes(tenant_id),\n is_default=(tenant_id == self._default_tenant_id),\n )\n```\n\n---\n\n## Tenant Isolation Patterns\n\nFramework M supports three tenant isolation strategies. Choose based on your security, compliance, and scalability requirements.\n\n### Pattern 1: Logical Isolation (Recommended)\n\n**Description:** All tenants share the same database and tables. Row-Level Security (RLS) filters ensure tenants only see their own data.\n\n**Implementation:**\n\n```python\nfrom framework_m.core.domain.base_doctype import BaseDocType\nfrom pydantic import Field\n\nclass Invoice(BaseDocType):\n \"\"\"Invoice DocType with tenant isolation.\"\"\"\n\n tenant_id: str = Field(max_length=100)\n customer_name: str = Field(max_length=200)\n amount: float\n\n class Meta:\n apply_rls: bool = True\n rls_field: str = \"tenant_id\" # Filter by this field\n```\n\n**How RLS Works:**\n\n1. `GenericRepository` automatically applies RLS filters:\n\n```python\n# User queries for invoices\ninvoices = await repo.list_entities(session, filters=user_filters)\n\n# Framework automatically adds: WHERE tenant_id = 'acme-corp'\n# User only sees their tenant's invoices\n```\n\n2. Admins can bypass RLS:\n\n```python\n# Admin user with is_system_user=True bypasses RLS\nadmin_invoices = await repo.list_entities(session, filters=[], user=admin_user)\n# Admin sees all invoices across all tenants\n```\n\n**Pros:**\n\n- \u2705 Simple to implement and manage\n- \u2705 Cost-effective (single database)\n- \u2705 Easy backups and maintenance\n- \u2705 Dynamic tenant creation (no schema changes)\n- \u2705 Framework handles filtering automatically\n\n**Cons:**\n\n- \u274c \"Noisy neighbor\" problem (one tenant can impact others)\n- \u274c Cannot customize schema per tenant\n- \u274c Compliance concerns for highly sensitive data\n\n**Best For:**\n\n- Most SaaS applications\n- B2B products with standard features\n- Startups and growing companies\n- When tenant data has similar structure\n\n### Pattern 2: Database-per-Tenant\n\n**Description:** Each tenant gets a separate database. Complete physical isolation.\n\n**Implementation:**\n\n```python\n# framework_config.toml\n[database]\nurl = \"postgresql://user:pass@localhost/framework_default\"\n\n# Tenant-specific database bindings\n[database.tenant_binds]\n\"acme-corp\" = \"postgresql://user:pass@localhost/framework_acme\"\n\"globex\" = \"postgresql://user:pass@localhost/framework_globex\"\n```\n\n```python\n# In repository or session factory\ndef get_database_url(tenant_id: str) -> str:\n \"\"\"Get database URL for tenant.\"\"\"\n from framework_m.cli.config import load_config\n\n config = load_config()\n tenant_binds = config.get(\"database\", {}).get(\"tenant_binds\", {})\n\n # Check for tenant-specific database\n if tenant_id in tenant_binds:\n return tenant_binds[tenant_id]\n\n # Fallback to default database\n return config[\"database\"][\"url\"]\n```\n\n**Pros:**\n\n- \u2705 Complete data isolation\n- \u2705 Can customize schema per tenant\n- \u2705 Easy to backup individual tenants\n- \u2705 No RLS overhead\n- \u2705 Can move tenant data to different servers\n\n**Cons:**\n\n- \u274c High operational complexity\n- \u274c More expensive (multiple databases)\n- \u274c Schema migrations across all databases\n- \u274c Connection pool management\n- \u274c Cross-tenant queries impossible\n\n**Best For:**\n\n- Enterprise customers demanding isolation\n- Regulated industries (healthcare, finance)\n- Customers with custom schema needs\n- When billing per database is acceptable\n\n### Pattern 3: Schema-per-Tenant (PostgreSQL)\n\n**Description:** All tenants in one database, but each gets a separate PostgreSQL schema.\n\n**Implementation:**\n\n```python\n# Set PostgreSQL search_path per tenant\nfrom sqlalchemy import text\n\nasync def set_tenant_schema(session, tenant_id: str):\n \"\"\"Set PostgreSQL schema for tenant.\"\"\"\n schema_name = f\"tenant_{tenant_id.replace('-', '_')}\"\n await session.execute(text(f\"SET search_path TO {schema_name}, public\"))\n```\n\n```python\n# In session provider\nfrom litestar import Request\n\nasync def provide_session(request: Request):\n \"\"\"Provide database session with tenant schema.\"\"\"\n tenant_ctx = request.state.tenant\n\n async with get_session() as session:\n # Set schema based on tenant\n await set_tenant_schema(session, tenant_ctx.tenant_id)\n yield session\n```\n\n**Pros:**\n\n- \u2705 Good isolation (separate schemas)\n- \u2705 Single database (easier management)\n- \u2705 Can customize schema per tenant\n- \u2705 Better than logical isolation for compliance\n\n**Cons:**\n\n- \u274c PostgreSQL-specific\n- \u274c Schema migrations across all schemas\n- \u274c Still shares database resources\n- \u274c Requires careful connection management\n\n**Best For:**\n\n- PostgreSQL-only deployments\n- Mid-tier isolation needs\n- When database-per-tenant is too expensive\n- Regulated data with moderate isolation needs\n\n### Comparison Table\n\n| Feature | Logical Isolation | Database-per-Tenant | Schema-per-Tenant |\n| --------------------- | ----------------- | ------------------- | ----------------- |\n| **Setup Complexity** | Low | High | Medium |\n| **Operational Cost** | Low | High | Medium |\n| **Data Isolation** | Application-level | Physical | Schema-level |\n| **Custom Schema** | \u274c No | \u2705 Yes | \u2705 Yes |\n| **Scalability** | \u2705 Excellent | \u26a0\ufe0f Limited | \u26a0\ufe0f Good |\n| **Compliance** | \u26a0\ufe0f Moderate | \u2705 Excellent | \u2705 Good |\n| **Framework Support** | \u2705 Built-in | \u26a0\ufe0f Manual | \u26a0\ufe0f Manual |\n\n---\n\n## Using TenantContext in Code\n\n### Dependency Injection Pattern\n\nFramework M provides `TenantContext` via dependency injection:\n\n```python\nfrom litestar import get, post, Request\nfrom framework_m.core.interfaces.tenant import TenantContext\n\n@get(\"/api/dashboard\")\nasync def get_dashboard(request: Request) -> dict:\n \"\"\"Get tenant-specific dashboard data.\"\"\"\n tenant_ctx: TenantContext = request.state.tenant\n\n return {\n \"tenant_id\": tenant_ctx.tenant_id,\n \"plan\": tenant_ctx.attributes.get(\"plan\", \"free\"),\n \"features\": tenant_ctx.attributes.get(\"features\", []),\n }\n```\n\n### Feature Flags from Tenant Attributes\n\n```python\n@post(\"/api/reports/advanced\")\nasync def generate_advanced_report(request: Request) -> dict:\n \"\"\"Generate advanced report (enterprise feature).\"\"\"\n tenant_ctx: TenantContext = request.state.tenant\n features = tenant_ctx.attributes.get(\"features\", [])\n\n if \"advanced_reports\" not in features:\n raise HTTPException(\n status_code=403,\n detail=\"Advanced reports not available in your plan\"\n )\n\n # Generate report...\n return {\"status\": \"success\"}\n```\n\n### Tenant-Aware Queries\n\nWhen using `GenericRepository`, RLS filters are applied automatically:\n\n```python\nfrom framework_m.adapters.db.generic_repository import GenericRepository\nfrom framework_m.core.doctypes.invoice import Invoice\n\n@get(\"/api/invoices\")\nasync def list_invoices(\n request: Request,\n user: UserContext,\n session: AsyncSession\n) -> list[dict]:\n \"\"\"List invoices for current tenant.\"\"\"\n repo = GenericRepository(Invoice)\n\n # RLS automatically filters: WHERE tenant_id = 'acme-corp'\n invoices = await repo.list_entities(session, user=user)\n\n return [inv.model_dump() for inv in invoices]\n```\n\n### Cross-Tenant Queries (Admin Only)\n\n```python\n@get(\"/api/admin/all-invoices\")\nasync def list_all_invoices(\n user: UserContext,\n session: AsyncSession\n) -> list[dict]:\n \"\"\"List invoices across all tenants (admin only).\"\"\"\n if not user.is_system_user:\n raise HTTPException(status_code=403, detail=\"Admin access required\")\n\n repo = GenericRepository(Invoice)\n\n # Admin bypasses RLS, sees all tenants\n all_invoices = await repo.list_entities(session, user=user)\n\n return [inv.model_dump() for inv in all_invoices]\n```\n\n### Manual Tenant Filtering\n\nIf you need manual control:\n\n```python\nfrom framework_m.core.interfaces.repository import FilterSpec, FilterOperator\n\n@get(\"/api/custom-query\")\nasync def custom_query(\n request: Request,\n session: AsyncSession\n) -> list[dict]:\n \"\"\"Custom query with manual tenant filtering.\"\"\"\n tenant_ctx: TenantContext = request.state.tenant\n repo = GenericRepository(Invoice)\n\n # Manual tenant filter\n filters = [\n FilterSpec(\n field=\"tenant_id\",\n operator=FilterOperator.EQ,\n value=tenant_ctx.tenant_id\n )\n ]\n\n invoices = await repo.list_entities(session, filters=filters)\n return [inv.model_dump() for inv in invoices]\n```\n\n---\n\n## Frontend Multi-Tenancy\n\n### Extracting Tenant from JWT\n\nIf your frontend receives a JWT token, extract tenant information:\n\n```typescript\n// src/utils/auth.ts\ninterface JWTPayload {\n sub: string;\n tenant_id: string;\n tenant_attributes: {\n plan: string;\n features: string[];\n max_users: number;\n };\n}\n\nexport function decodeToken(token: string): JWTPayload {\n const base64Url = token.split(\".\")[1];\n const base64 = base64Url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split(\"\")\n .map(c => \"%\" + (\"00\" + c.charCodeAt(0).toString(16)).slice(-2))\n .join(\"\"),\n );\n return JSON.parse(jsonPayload);\n}\n```\n\n### React Context for Tenant\n\n```typescript\n// src/contexts/TenantContext.tsx\nimport { createContext, useContext, ReactNode } from 'react';\n\ninterface TenantAttributes {\n plan: string;\n features: string[];\n max_users?: number;\n}\n\ninterface TenantContextType {\n tenantId: string;\n attributes: TenantAttributes;\n hasFeature: (feature: string) => boolean;\n}\n\nconst TenantContext = createContext(null);\n\nexport function TenantProvider({ children }: { children: ReactNode }) {\n // Extract from JWT or API response\n const token = localStorage.getItem('access_token');\n const payload = token ? decodeToken(token) : null;\n\n const value: TenantContextType = {\n tenantId: payload?.tenant_id || 'default',\n attributes: payload?.tenant_attributes || { plan: 'free', features: [] },\n hasFeature: (feature: string) =>\n payload?.tenant_attributes.features.includes(feature) || false,\n };\n\n return (\n \n {children}\n \n );\n}\n\nexport function useTenant() {\n const context = useContext(TenantContext);\n if (!context) {\n throw new Error('useTenant must be used within TenantProvider');\n }\n return context;\n}\n```\n\n### Feature-Gated Components\n\n```typescript\n// src/components/FeatureGate.tsx\nimport { useTenant } from '../contexts/TenantContext';\n\ninterface FeatureGateProps {\n feature: string;\n children: ReactNode;\n fallback?: ReactNode;\n}\n\nexport function FeatureGate({ feature, children, fallback }: FeatureGateProps) {\n const { hasFeature } = useTenant();\n\n if (!hasFeature(feature)) {\n return fallback ||
This feature is not available in your plan.
;\n }\n\n return <>{children};\n}\n```\n\n```typescript\n// Usage in components\nimport { FeatureGate } from './components/FeatureGate';\n\nfunction Dashboard() {\n return (\n
\n

Dashboard

\n\n \n \n \n\n \n \n \n
\n );\n}\n```\n\n### Tenant-Specific Branding\n\n```typescript\n// src/hooks/useTenantBranding.ts\nimport { useTenant } from \"../contexts/TenantContext\";\n\ninterface BrandingConfig {\n logo: string;\n primaryColor: string;\n companyName: string;\n}\n\nexport function useTenantBranding(): BrandingConfig {\n const { attributes } = useTenant();\n\n return {\n logo: attributes.logo_url || \"/default-logo.svg\",\n primaryColor: attributes.primary_color || \"#3b82f6\",\n companyName: attributes.company_name || \"Framework M\",\n };\n}\n```\n\n```typescript\n// Usage in Navbar\nfunction Navbar() {\n const branding = useTenantBranding();\n\n return (\n \n );\n}\n```\n\n### Tenant Selector (Admin UI)\n\nFor admin users managing multiple tenants:\n\n```typescript\n// src/components/TenantSelector.tsx\nimport { useState, useEffect } from 'react';\n\nexport function TenantSelector() {\n const [tenants, setTenants] = useState([]);\n const [currentTenant, setCurrentTenant] = useState('');\n\n useEffect(() => {\n // Fetch available tenants (admin only)\n fetch('/api/admin/tenants')\n .then(res => res.json())\n .then(data => setTenants(data.tenants));\n }, []);\n\n const switchTenant = (tenantId: string) => {\n // Store selected tenant in localStorage\n localStorage.setItem('admin_selected_tenant', tenantId);\n setCurrentTenant(tenantId);\n\n // Reload data for new tenant\n window.location.reload();\n };\n\n return (\n \n );\n}\n```\n\n---\n\n## Switching Adapters\n\n### Configuration-Based Selection\n\nThe simplest approach is to use `create_tenant_adapter_from_headers()`, which automatically selects the adapter based on `framework_config.toml`:\n\n```python\nfrom framework_m.adapters.tenant import create_tenant_adapter_from_headers\n\n# In Litestar dependency provider\nasync def provide_tenant_adapter(request: Request):\n \"\"\"Provide tenant adapter based on configuration.\"\"\"\n adapter = create_tenant_adapter_from_headers(\n headers=request.headers if request else None\n )\n return adapter\n```\n\n### Factory Logic\n\n```python\n# framework-m/src/framework_m/adapters/tenant.py\ndef create_tenant_adapter_from_headers(\n headers: Mapping[str, str] | None = None,\n) -> ImplicitTenantAdapter | HeaderTenantAdapter:\n \"\"\"Create appropriate tenant adapter based on config.\"\"\"\n from framework_m.core.interfaces.tenant import is_multi_tenant\n\n # If multi-tenant mode AND headers provided, use HeaderTenantAdapter\n if is_multi_tenant() and headers is not None:\n return HeaderTenantAdapter(headers=headers)\n\n # Otherwise use ImplicitTenantAdapter (single-tenant mode)\n return ImplicitTenantAdapter()\n```\n\n### Environment Variable\n\n```bash\n# .env or framework_config.toml\nTENANT_MODE=multi # or \"single\"\n```\n\n```toml\n# framework_config.toml\n[tenancy]\nmode = \"multi\" # Uses HeaderTenantAdapter\n# OR\nmode = \"single\" # Uses ImplicitTenantAdapter\n```\n\n### Custom Adapter via Entrypoint\n\nFor advanced use cases, register a custom tenant adapter:\n\n```python\n# my_app/custom_tenant_adapter.py\nfrom framework_m.core.interfaces.tenant import TenantProtocol, TenantContext\n\nclass DatabaseTenantAdapter:\n \"\"\"Load tenant from database instead of headers.\"\"\"\n\n def __init__(self, request_id: str):\n self.request_id = request_id\n\n def get_current_tenant(self) -> str:\n # Look up tenant from database using request_id\n return lookup_tenant_from_db(self.request_id)\n\n def get_tenant_attributes(self, tenant_id: str) -> dict:\n # Fetch attributes from database\n return fetch_tenant_attributes(tenant_id)\n```\n\nRegister via entrypoint:\n\n```python\n# setup.py or pyproject.toml\n[project.entry-points.\"framework_m.tenant_adapters\"]\ndatabase = \"my_app.custom_tenant_adapter:DatabaseTenantAdapter\"\n```\n\n### Testing with MockTenantAdapter\n\n```python\n# tests/conftest.py\nimport pytest\nfrom framework_m.core.interfaces.tenant import TenantContext\n\nclass MockTenantAdapter:\n \"\"\"Mock adapter for testing.\"\"\"\n\n def __init__(self, tenant_id: str = \"test-tenant\"):\n self.tenant_id = tenant_id\n\n def get_current_tenant(self) -> str:\n return self.tenant_id\n\n def get_tenant_attributes(self, tenant_id: str) -> dict:\n return {\n \"plan\": \"enterprise\",\n \"features\": [\"all\"],\n }\n\n def get_context(self) -> TenantContext:\n return TenantContext(\n tenant_id=self.tenant_id,\n attributes=self.get_tenant_attributes(self.tenant_id),\n is_default=False,\n )\n\n@pytest.fixture\ndef mock_tenant_adapter():\n \"\"\"Provide mock tenant adapter for tests.\"\"\"\n return MockTenantAdapter(tenant_id=\"test-tenant\")\n```\n\n---\n\n## Best Practices\n\n### 1. Always Use TenantContext from Request State\n\n```python\n# \u2705 Good: Use request.state.tenant\nasync def my_endpoint(request: Request):\n tenant_ctx = request.state.tenant\n tenant_id = tenant_ctx.tenant_id\n\n# \u274c Bad: Don't try to resolve tenant manually\nasync def my_endpoint(request: Request):\n tenant_id = request.headers.get(\"x-tenant-id\") # Don't do this!\n```\n\n### 2. Rely on RLS for Data Isolation\n\n```python\n# \u2705 Good: Let framework handle tenant filtering\ninvoices = await repo.list_entities(session, user=user)\n\n# \u274c Bad: Manual filtering (fragile and error-prone)\ninvoices = await session.execute(\n select(Invoice).where(Invoice.tenant_id == tenant_id)\n)\n```\n\n### 3. Test Both Tenancy Modes\n\n```python\n# Test single-tenant mode\n@pytest.mark.parametrize(\"tenancy_mode\", [\"single\"])\ndef test_single_tenant(tenancy_mode):\n adapter = ImplicitTenantAdapter()\n assert adapter.get_current_tenant() == \"default\"\n\n# Test multi-tenant mode\n@pytest.mark.parametrize(\"tenancy_mode\", [\"multi\"])\ndef test_multi_tenant(tenancy_mode):\n headers = {\"x-tenant-id\": \"acme\"}\n adapter = HeaderTenantAdapter(headers=headers)\n assert adapter.get_current_tenant() == \"acme\"\n```\n\n### 4. Document Tenant Attributes Schema\n\n```python\n# Define expected tenant attributes in documentation\n\"\"\"\nTenant Attributes Schema:\n{\n \"plan\": str, # \"free\" | \"pro\" | \"enterprise\"\n \"features\": list[str], # [\"reports\", \"api\", \"webhooks\"]\n \"max_users\": int, # Maximum users allowed\n \"max_storage_gb\": int, # Storage quota in GB\n \"custom_domain\": str, # Optional custom domain\n}\n\"\"\"\n```\n\n### 5. Handle Missing Tenant Gracefully\n\n```python\nasync def get_tenant_safely(request: Request) -> TenantContext:\n \"\"\"Get tenant context with fallback.\"\"\"\n try:\n return request.state.tenant\n except AttributeError:\n # Fallback for public endpoints\n return TenantContext(\n tenant_id=\"public\",\n attributes={\"plan\": \"free\", \"features\": []},\n is_default=True,\n )\n```\n\n---\n\n## Migration Path\n\n### From Single-Tenant to Multi-Tenant\n\nIf you start with Mode A and need to migrate to Mode B:\n\n1. **Add tenant_id column to all DocTypes:**\n\n```python\n# Add to all DocTypes that need isolation\nclass Invoice(BaseDocType):\n tenant_id: str = Field(max_length=100, default=\"default\")\n\n class Meta:\n apply_rls = True\n rls_field = \"tenant_id\"\n```\n\n2. **Generate migration:**\n\n```bash\nm migrate generate \"add_tenant_id_to_all_tables\"\n```\n\n3. **Update existing data:**\n\n```python\n# In migration file\ndef upgrade():\n # Set all existing rows to \"default\" tenant\n op.execute(\"UPDATE invoice SET tenant_id = 'default' WHERE tenant_id IS NULL\")\n```\n\n4. **Switch configuration:**\n\n```toml\n[tenancy]\nmode = \"multi\" # Switch from \"single\" to \"multi\"\n```\n\n5. **Deploy gateway:**\n\nSet up Keycloak/Auth0 to populate `X-Tenant-ID` headers.\n\n6. **Test thoroughly:**\n\nEnsure existing \"default\" tenant users still have access.\n\n---\n\n## Troubleshooting\n\n### Tenant Not Resolved\n\n**Symptom:** `request.state.tenant` is None or missing\n\n**Solution:**\n\n1. Check `framework_config.toml` has `[tenancy]` section\n2. Verify `mode = \"multi\"` if using HeaderTenantAdapter\n3. Ensure gateway is populating `X-Tenant-ID` header\n4. Check middleware order (tenant middleware must run early)\n\n### Users See Wrong Tenant's Data\n\n**Symptom:** Users see data from other tenants\n\n**Solution:**\n\n1. Verify `apply_rls = True` in DocType Meta\n2. Check `rls_field` points to correct column (usually `tenant_id`)\n3. Ensure queries use `GenericRepository` (not raw SQLAlchemy)\n4. Verify user context has correct tenant_id\n\n### Feature Flags Not Working\n\n**Symptom:** Features available when they shouldn't be\n\n**Solution:**\n\n1. Check `X-Tenant-Attributes` header is populated\n2. Verify JSON is valid in attributes header\n3. Ensure frontend checks `tenant.attributes.features` array\n4. Log `tenant_ctx.attributes` to debug\n\n### Performance Issues\n\n**Symptom:** Slow queries in multi-tenant mode\n\n**Solution:**\n\n1. Add database index on `tenant_id` column:\n\n```sql\nCREATE INDEX idx_invoice_tenant_id ON invoice(tenant_id);\n```\n\n2. Use database-per-tenant for large tenants\n3. Consider read replicas for heavy tenants\n4. Cache tenant attributes in Redis\n\n---\n\n## Summary\n\nFramework M's multi-tenancy system provides:\n\n- **Two modes**: Single-tenant (ImplicitTenantAdapter) and multi-tenant (HeaderTenantAdapter)\n- **Three isolation patterns**: Logical (RLS), database-per-tenant, schema-per-tenant\n- **Automatic RLS**: Framework handles tenant filtering transparently\n- **Feature flags**: Per-tenant attributes control feature availability\n- **Easy testing**: Mock adapters for unit tests\n- **Migration path**: Start single, scale to multi-tenant\n\nChoose the mode and isolation pattern that fits your requirements, and let Framework M handle the complexity.\n", "metadata": {"path": "docs/developer/tenancy-modes.md"}} {"id": "file-docs-developer-theming.md", "type": "doc", "title": "Document: docs/developer/theming.md", "content": "---\ntitle: Theming\nsidebar_position: 8\n---\n\n# Theming in Framework M\n\nThis guide covers the theming system in Framework M's frontend, including light/dark mode, custom color schemes, and user preference management.\n\n---\n\n## Table of Contents\n\n1. [Overview](#overview)\n2. [Light/Dark Mode](#lightdark-mode)\n3. [Using the Theme System](#using-the-theme-system)\n4. [Custom Color Schemes](#custom-color-schemes)\n5. [CSS Variables Reference](#css-variables-reference)\n6. [Best Practices](#best-practices)\n\n---\n\n## Overview\n\nFramework M includes a built-in theming system that provides:\n\n- **Light/Dark Mode**: Automatic theme switching with user preference persistence\n- **System Theme Detection**: Respects OS-level dark mode preferences\n- **LocalStorage Persistence**: User preferences saved across sessions\n- **CSS Variables**: Easy customization of all colors and styles\n- **React Context**: Theme state accessible throughout the app\n\n### Features\n\n\u2705 **Automatic Detection**: Detects system theme preference on first load\n\u2705 **Toggle Component**: Icon-based theme switcher in navbar\n\u2705 **Smooth Transitions**: CSS transitions for theme changes\n\u2705 **Persistent**: Saves preference in localStorage\n\u2705 **Accessible**: Keyboard navigation and ARIA labels\n\n---\n\n## Light/Dark Mode\n\n### How It Works\n\nThe theme system uses a React Context (`ThemeContext`) to manage the current theme state and CSS variables for styling.\n\n**Theme Resolution Order:**\n\n1. **LocalStorage**: Checks for saved user preference (`framework-m-theme`)\n2. **System Preference**: Uses `prefers-color-scheme` media query\n3. **Default**: Falls back to `light` theme\n\n**CSS Implementation:**\n\nThe `dark` class is applied to the `` element when dark mode is active:\n\n```html\n\n\n \n \n\n```\n\nAll color variables are defined in `:root` for light mode and `.dark` for dark mode in `App.css`.\n\n---\n\n## Using the Theme System\n\n### Basic Usage\n\nImport and use the `useTheme` hook in any component:\n\n```typescript\nimport { useTheme } from '../contexts/ThemeContext';\n\nfunction MyComponent() {\n const { theme, setTheme, toggleTheme } = useTheme();\n\n return (\n
\n

Current theme: {theme}

\n \n \n \n
\n );\n}\n```\n\n### ThemeContext API\n\n```typescript\ninterface ThemeContextType {\n theme: Theme; // Current theme: \"light\" | \"dark\"\n setTheme: (theme: Theme) => void; // Set theme explicitly\n toggleTheme: () => void; // Toggle between light and dark\n}\n```\n\n### ThemeProvider Setup\n\nThe `ThemeProvider` is already configured in `App.tsx`:\n\n```typescript\n// src/App.tsx\nimport { ThemeProvider } from './contexts/ThemeContext';\n\nexport const App = () => (\n \n \n \n \n \n);\n```\n\n### ThemeToggle Component\n\nThe navbar includes a `ThemeToggle` button:\n\n```typescript\n// src/layout/Navbar.tsx\nimport { ThemeToggle } from '../components/ThemeToggle';\n\nexport function Navbar() {\n return (\n
\n {/* Other navbar items */}\n \n
\n );\n}\n```\n\n**Features:**\n\n- Sun icon in dark mode (click to switch to light)\n- Moon icon in light mode (click to switch to dark)\n- Tooltip showing current theme\n- Keyboard accessible\n- Smooth hover/active states\n\n---\n\n## Custom Color Schemes\n\n### Using CSS Variables\n\nAll colors are defined as CSS variables. To customize, override them:\n\n```css\n/* Custom color scheme */\n:root {\n --color-primary: #8b5cf6; /* Purple primary */\n --color-primary-dark: #7c3aed;\n --color-success: #10b981; /* Emerald green */\n}\n\n.dark {\n --color-primary: #a78bfa; /* Lighter purple for dark mode */\n --color-primary-dark: #8b5cf6;\n}\n```\n\n### Per-Component Styling\n\nUse CSS variables in your components:\n\n```typescript\nfunction CustomButton() {\n return (\n \n Click Me\n \n );\n}\n```\n\n### Theme-Aware Components\n\nCreate components that respond to theme changes:\n\n```typescript\nfunction ThemedCard({ children }: { children: React.ReactNode }) {\n const { theme } = useTheme();\n\n return (\n \n {children}\n \n );\n}\n```\n\n---\n\n## CSS Variables Reference\n\n### Light Mode Colors (`:root`)\n\n```css\n--color-bg: #ffffff; /* Main background */\n--color-bg-secondary: #f8fafc; /* Secondary background */\n--color-bg-tertiary: #f1f5f9; /* Tertiary background */\n--color-border: #e2e8f0; /* Border color */\n--color-text: #0f172a; /* Primary text */\n--color-text-secondary: #64748b; /* Secondary text */\n--color-text-muted: #94a3b8; /* Muted text */\n--color-primary: #0ea5e9; /* Primary brand color */\n--color-primary-dark: #0284c7; /* Primary hover/active */\n--color-sidebar: #1e293b; /* Sidebar background */\n--color-sidebar-text: #e2e8f0; /* Sidebar text */\n--color-sidebar-hover: #334155; /* Sidebar hover state */\n--color-success: #22c55e; /* Success state */\n--color-warning: #f59e0b; /* Warning state */\n--color-error: #ef4444; /* Error state */\n```\n\n### Dark Mode Colors (`.dark`)\n\n```css\n--color-bg: #0f172a; /* Main background (dark) */\n--color-bg-secondary: #1e293b; /* Secondary background (dark) */\n--color-bg-tertiary: #334155; /* Tertiary background (dark) */\n--color-border: #334155; /* Border color (dark) */\n--color-text: #f1f5f9; /* Primary text (light) */\n--color-text-secondary: #cbd5e1; /* Secondary text (light) */\n--color-text-muted: #94a3b8; /* Muted text */\n--color-primary: #38bdf8; /* Primary brand (lighter) */\n--color-primary-dark: #0ea5e9; /* Primary hover/active */\n--color-sidebar: #020617; /* Sidebar background (darker) */\n--color-sidebar-text: #e2e8f0; /* Sidebar text */\n--color-sidebar-hover: #1e293b; /* Sidebar hover state */\n```\n\n### Spacing\n\n```css\n--spacing-xs: 0.25rem; /* 4px */\n--spacing-sm: 0.5rem; /* 8px */\n--spacing-md: 1rem; /* 16px */\n--spacing-lg: 1.5rem; /* 24px */\n--spacing-xl: 2rem; /* 32px */\n```\n\n### Border Radius\n\n```css\n--radius-sm: 4px;\n--radius-md: 6px;\n--radius-lg: 8px;\n```\n\n### Shadows\n\n```css\n/* Light mode */\n--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);\n\n/* Dark mode */\n--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);\n--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);\n```\n\n### Typography\n\n```css\n--font-sans:\n \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n--font-mono: \"JetBrains Mono\", \"Fira Code\", monospace;\n```\n\n---\n\n## Best Practices\n\n### 1. Always Use CSS Variables\n\n```typescript\n// \u2705 Good: Uses CSS variable\n
\n\n// \u274c Bad: Hardcoded color\n
\n```\n\n### 2. Test in Both Themes\n\nAlways test your components in both light and dark mode:\n\n```typescript\n// Use the theme toggle in navbar to switch modes\n// Ensure text is readable and colors have good contrast\n```\n\n### 3. Use Semantic Color Names\n\n```css\n/* \u2705 Good: Semantic names */\n--color-success: #22c55e;\n--color-error: #ef4444;\n\n/* \u274c Bad: Color-based names */\n--color-green: #22c55e;\n--color-red: #ef4444;\n```\n\n### 4. Respect User Preference\n\n```typescript\n// \u2705 Good: Let users choose\nconst { theme, toggleTheme } = useTheme();\n\n// \u274c Bad: Force a theme\ndocument.documentElement.classList.add(\"dark\"); // Don't do this\n```\n\n### 5. Provide Visual Feedback\n\n```css\n.theme-toggle {\n transition: all 0.2s ease;\n}\n\n.theme-toggle:hover {\n background: var(--hover-bg);\n}\n\n.theme-toggle:active {\n transform: scale(0.95);\n}\n```\n\n### 6. Consider Accessibility\n\n```typescript\n// Include ARIA labels\n\n {/* Icon */}\n\n```\n\n---\n\n## Advanced Usage\n\n### Custom Theme Provider\n\nCreate a custom theme provider for additional themes:\n\n```typescript\n// src/contexts/CustomThemeContext.tsx\ntype ExtendedTheme = 'light' | 'dark' | 'blue' | 'purple';\n\nexport function CustomThemeProvider({ children }: { children: ReactNode }) {\n const [theme, setTheme] = useState('light');\n\n useEffect(() => {\n // Apply custom theme classes\n document.documentElement.className = theme;\n }, [theme]);\n\n return (\n \n {children}\n \n );\n}\n```\n\n```css\n/* Define custom themes */\n.blue {\n --color-primary: #3b82f6;\n --color-bg: #eff6ff;\n}\n\n.purple {\n --color-primary: #8b5cf6;\n --color-bg: #faf5ff;\n}\n```\n\n### Syncing with Backend\n\nStore user theme preference in backend:\n\n```typescript\nimport { useUpdate } from \"@refinedev/core\";\n\nfunction ThemeSyncer() {\n const { theme } = useTheme();\n const { mutate } = useUpdate();\n\n useEffect(() => {\n // Save to UserPreferences\n mutate({\n resource: \"UserPreferences\",\n id: \"current\",\n values: { theme },\n });\n }, [theme, mutate]);\n\n return null;\n}\n```\n\n### Per-Tenant Theming\n\nCombine with tenant context for custom branding:\n\n```typescript\nimport { useTenant } from '../contexts/TenantContext';\nimport { useTheme } from '../contexts/ThemeContext';\n\nfunction BrandedApp() {\n const { attributes } = useTenant();\n const { theme } = useTheme();\n\n useEffect(() => {\n // Apply tenant brand colors\n if (attributes.primary_color) {\n document.documentElement.style.setProperty(\n '--color-primary',\n attributes.primary_color\n );\n }\n }, [attributes]);\n\n return
{/* App content */}
;\n}\n```\n\n---\n\n## Troubleshooting\n\n### Theme Not Persisting\n\n**Problem:** Theme resets to light on page reload\n\n**Solution:** Check localStorage is enabled:\n\n```typescript\n// Test localStorage\ntry {\n localStorage.setItem(\"test\", \"test\");\n localStorage.removeItem(\"test\");\n console.log(\"localStorage is working\");\n} catch (e) {\n console.error(\"localStorage is disabled\");\n}\n```\n\n### Flicker on Page Load\n\n**Problem:** Brief flash of light theme before dark mode applies\n\n**Solution:** Add inline script in `index.html` before React loads:\n\n```html\n\n\n\n \n \n \n \n
\n \n\n```\n\n### Colors Not Updating\n\n**Problem:** CSS variables not reflecting in components\n\n**Solution:** Ensure you're using `var()` syntax:\n\n```css\n/* \u2705 Correct */\nbackground: var(--color-bg);\n\n/* \u274c Wrong */\nbackground: --color-bg;\n```\n\n---\n\n## Summary\n\nFramework M's theming system provides:\n\n- \u2705 **Light/Dark Mode**: Built-in toggle with user preference\n- \u2705 **System Detection**: Respects OS theme preference\n- \u2705 **Persistent**: Saves to localStorage\n- \u2705 **Customizable**: CSS variables for all colors\n- \u2705 **Accessible**: Keyboard navigation and ARIA support\n- \u2705 **Extensible**: Easy to add custom themes\n\nThe system is production-ready and can be extended for tenant-specific branding, custom color schemes, or additional theme variants.\n", "metadata": {"path": "docs/developer/theming.md"}} {"id": "file-docs-developer-creating-apps.md", "type": "doc", "title": "Document: docs/developer/creating-apps.md", "content": "---\ntitle: Creating Apps\nsidebar_position: 1\n---\n\n# Creating Apps\n\nA guide to building Framework M applications.\n\n## Prerequisites\n\n- **Python 3.12+**\n- **uv** package manager\n\n## Installation\n\nTo build applications, you need the **Framework M Studio** package which provides development tooling and scaffolding. This should be installed as a development dependency:\n\n```bash\n# Add framework\nuv add framework-m\n\n# Add dev tools\nuv add --dev framework-m-studio\n```\n\n## Create a New App\n\nThe `m` CLI provides scaffolding commands to create new apps.\n\n```bash\n# From your project root\nuv run m new:app my_crm\n```\n\nThis creates:\n```\nmy_crm/\n\u251c\u2500\u2500 src/\n\u2502 \u2514\u2500\u2500 my_crm/ # Namespaced package\n\u2502 \u251c\u2500\u2500 __init__.py\n\u2502 \u2514\u2500\u2500 doctypes/\n\u2502 \u2514\u2500\u2500 __init__.py\n\u251c\u2500\u2500 pyproject.toml\n\u2514\u2500\u2500 README.md\n```\n\n## Add a DocType\n\n```bash\ncd my_crm\nuv sync\nuv run m new:doctype Contact\n```\n\nThis creates:\n```\nsrc/my_crm/doctypes/contact/\n\u251c\u2500\u2500 __init__.py\n\u251c\u2500\u2500 doctype.py # Schema definition\n\u2514\u2500\u2500 controller.py # Business logic\n\ntests/doctypes/contact/\n\u2514\u2500\u2500 test_contact.py # Tests\n```\n\n## Define Fields\n\nEdit `src/my_crm/doctypes/contact/doctype.py`:\n\n```python\nfrom __future__ import annotations\n\nfrom typing import ClassVar\nfrom pydantic import Field\nfrom framework_m.core.domain.base_doctype import BaseDocType\n\n\nclass Contact(BaseDocType):\n \"\"\"Customer contact information.\"\"\"\n\n __doctype_name__: ClassVar[str] = \"Contact\"\n\n # Required fields\n first_name: str = Field(description=\"First name\")\n last_name: str = Field(description=\"Last name\")\n\n # Optional fields\n email: str = Field(default=\"\", description=\"Email\")\n phone: str = Field(default=\"\", description=\"Phone\")\n company: str = Field(default=\"\", description=\"Company name\")\n\n class Meta:\n naming_rule: ClassVar[str] = \"autoincrement\"\n```\n\n## Add Business Logic\n\nEdit `src/doctypes/contact/controller.py`:\n\n```python\nfrom __future__ import annotations\n\nfrom framework_m.core.domain.base_controller import BaseController\nfrom .doctype import Contact\n\n\nclass ContactController(BaseController[Contact]):\n \"\"\"Contact business logic.\"\"\"\n\n async def validate(self, context=None):\n \"\"\"Validate before saving.\"\"\"\n if not self.doc.first_name.strip():\n raise ValueError(\"First name is required\")\n\n # Normalize email\n if self.doc.email:\n self.doc.email = self.doc.email.lower().strip()\n\n async def after_save(self, context=None):\n \"\"\"Called after save.\"\"\"\n # Example: Log or send notification\n pass\n```\n\n## Run Migrations\n\n```bash\n# Initialize Alembic (first time)\nuv run m migrate init\n\n# Create migration for your DocType\nuv run m migrate create \"Add Contact doctype\" --autogenerate\n\n# Apply migration\nuv run m migrate\n```\n\n## Start Studio\n\n```bash\nuv run m studio\n```\n\nOpen http://localhost:9000 to manage your data.\n\n## Test Your DocType\n\nEdit `src/doctypes/contact/test_contact.py`:\n\n```python\nimport pytest\nfrom .doctype import Contact\nfrom .controller import ContactController\n\n\ndef test_contact_creation():\n contact = Contact(\n first_name=\"John\",\n last_name=\"Doe\",\n email=\"John@Example.com\",\n )\n assert contact.first_name == \"John\"\n\n\n@pytest.mark.asyncio\nasync def test_validate_normalizes_email():\n contact = Contact(\n first_name=\"John\",\n last_name=\"Doe\",\n email=\"John@Example.com\",\n )\n controller = ContactController(contact)\n await controller.validate()\n\n assert contact.email == \"john@example.com\"\n\n\n@pytest.mark.asyncio\nasync def test_validate_rejects_empty_name():\n contact = Contact(\n first_name=\" \",\n last_name=\"Doe\",\n )\n controller = ContactController(contact)\n\n with pytest.raises(ValueError, match=\"First name is required\"):\n await controller.validate()\n```\n\nRun tests:\n```bash\nuv run pytest src/doctypes/contact/\n```\n\n## Multiple DocTypes\n\nFor related DocTypes, create a directory structure:\n\n```\nsrc/my_crm/doctypes/\n\u251c\u2500\u2500 __init__.py\n\u251c\u2500\u2500 contact/\n\u2502 \u251c\u2500\u2500 doctype.py\n\u2502 \u2514\u2500\u2500 controller.py\n\u251c\u2500\u2500 lead/\n\u2502 \u251c\u2500\u2500 doctype.py\n\u2502 \u2514\u2500\u2500 controller.py\n\u2514\u2500\u2500 opportunity/\n \u251c\u2500\u2500 doctype.py\n \u2514\u2500\u2500 controller.py\n```\n\n## Next Steps\n\n- [Defining DocTypes](./defining-doctypes.md) - Advanced patterns\n- [Architecture](./architecture.md) - System overview\n", "metadata": {"path": "docs/developer/creating-apps.md"}} {"id": "file-docs-developer-locale-resolution.md", "type": "doc", "title": "Document: docs/developer/locale-resolution.md", "content": "---\ntitle: Locale Resolution\nsidebar_position: 4\n---\n\n# Locale Resolution and Field Label Translation\nand how field labels are automatically translated in the Meta API.\n\n## Locale Resolution Order\n\nThe system resolves the user's locale in the following order:\n\n1. **Accept-Language header** - Browser language preference\n2. **User preference** - user.locale field in LocalUser/UserPreferences\n3. **Tenant default** - tenant.attributes.default_locale\n4. **System default** - DEFAULT_LOCALE setting (defaults to \"en\")\n\n## Usage Examples\n\n### 1. Frontend: Setting Locale via Accept-Language Header\n\n```typescript\n// Frontend automatically sends Accept-Language header\nfetch(\"http://localhost:8000/api/meta/Invoice\", {\n headers: {\n \"Accept-Language\": \"hi-IN,hi;q=0.9,en;q=0.8\",\n },\n});\n\n// The middleware parses this and resolves locale to 'hi'\n```\n\n### 2. Backend: User Preference\n\n```python\n# Set user's locale preference\nuser = LocalUser(\n email=\"john@example.com\",\n password_hash=\"...\",\n locale=\"hi\" # Hindi preference\n)\n\n# When this user makes requests, locale=\"hi\" takes precedence\n# over Accept-Language header\n```\n\n### 3. Backend: Tenant Default Locale\n\n```python\n# Configure tenant with default locale\ntenant = TenantContext(\n tenant_id=\"acme-corp\",\n attributes={\n \"default_locale\": \"ta\", # Tamil\n \"plan\": \"enterprise\"\n }\n)\n\n# All users in this tenant default to Tamil\n# unless they have a personal preference\n```\n\n### 4. Backend: Accessing Resolved Locale\n\n```python\nfrom litestar import get, Request\nfrom framework_m.adapters.web.middleware.locale import provide_locale\n\n@get(\"/api/greeting\")\nasync def get_greeting(locale: str = provide_locale) -> str:\n \\\"\\\"\\\"Greet user in their preferred language.\\\"\\\"\\\"\n greetings = {\n \"en\": \"Hello!\",\n \"hi\": \"\u0928\u092e\u0938\u094d\u0924\u0947!\",\n \"ta\": \"\u0bb5\u0ba3\u0b95\u0bcd\u0b95\u0bae\u0bcd!\",\n }\n return greetings.get(locale, greetings[\"en\"])\n```\n\n## Field Label Translation\n\nField labels (Field.description and Field.title) are automatically\ntranslated in the Meta API response based on the resolved locale.\n\n### Example: Creating Translations\n\n```python\n# 1. Create Translation entries\nfrom framework_m.core.doctypes.translation import Translation\n\n# English (original)\nTranslation(\n source_text=\"Customer Name\",\n translated_text=\"Customer Name\",\n locale=\"en\",\n context=\"field_label\"\n)\n\n# Hindi translation\nTranslation(\n source_text=\"Customer Name\",\n translated_text=\"\u0917\u094d\u0930\u093e\u0939\u0915 \u0915\u093e \u0928\u093e\u092e\",\n locale=\"hi\",\n context=\"field_label\"\n)\n\n# Tamil translation\nTranslation(\n source_text=\"Customer Name\",\n translated_text=\"\u0bb5\u0bbe\u0b9f\u0bbf\u0b95\u0bcd\u0b95\u0bc8\u0baf\u0bbe\u0bb3\u0bb0\u0bcd \u0baa\u0bc6\u0baf\u0bb0\u0bcd\",\n locale=\"ta\",\n context=\"field_label\"\n)\n```\n\n### Example: DocType with Translatable Fields\n\n```python\nfrom pydantic import Field\nfrom framework_m.core.domain.base_doctype import BaseDocType\n\nclass Invoice(BaseDocType):\n \\\"\\\"\\\"Invoice DocType with translatable field labels.\\\"\\\"\\\"\n\n customer: str = Field(\n description=\"Customer Name\", # Will be translated\n min_length=1,\n max_length=200\n )\n\n total: float = Field(\n description=\"Total Amount\", # Will be translated\n ge=0\n )\n```\n\n### Example: Meta API Response (Hindi locale)\n\n```bash\n# Request with Hindi locale\ncurl -H \"Accept-Language: hi\" http://localhost:8000/api/meta/Invoice\n\n# Response (field labels translated)\n{\n \"doctype\": \"Invoice\",\n \"locale\": \"hi\",\n \"schema\": {\n \"properties\": {\n \"customer\": {\n \"type\": \"string\",\n \"description\": \"\u0917\u094d\u0930\u093e\u0939\u0915 \u0915\u093e \u0928\u093e\u092e\", # Translated!\n \"minLength\": 1,\n \"maxLength\": 200\n },\n \"total\": {\n \"type\": \"number\",\n \"description\": \"\u0915\u0941\u0932 \u0930\u093e\u0936\u093f\", # Translated!\n \"minimum\": 0\n }\n }\n }\n}\n```\n\n## Middleware Configuration\n\nAdd the locale resolution middleware to your Litestar app:\n\n```python\nfrom litestar import Litestar\nfrom framework_m.adapters.web.middleware.locale import create_locale_middleware\n\napp = Litestar(\n route_handlers=[...],\n middleware=[\n create_locale_middleware(),\n # ... other middleware\n ]\n)\n```\n\n## Translation Context\n\nThe `context` field in Translation DocType allows disambiguation:\n\n```python\n# Button label\nTranslation(\n source_text=\"Save\",\n translated_text=\"\u0938\u0939\u0947\u091c\u0947\u0902\",\n locale=\"hi\",\n context=\"button\" # Specific to buttons\n)\n\n# Field label\nTranslation(\n source_text=\"Save\",\n translated_text=\"\u092c\u091a\u0924\",\n locale=\"hi\",\n context=\"field_label\" # Different meaning for field labels\n)\n```\n\n## Benefits\n\n1. **Automatic**: No manual translation code needed in endpoints\n2. **Flexible**: Multiple fallback levels ensure graceful degradation\n3. **User-friendly**: Respects browser language, user preference, and tenant defaults\n4. **Consistent**: Same locale resolution logic across all endpoints\n5. **Cacheable**: DefaultI18nAdapter caches translations in memory\n\n## See Also\n\n- `framework_m.adapters.web.middleware.locale` - Middleware implementation\n- `framework_m.adapters.i18n.DefaultI18nAdapter` - Translation adapter\n- `framework_m.core.doctypes.translation` - Translation DocType\n- `framework_m.core.doctypes.user` - User.locale field\n- `framework_m.core.interfaces.tenant` - TenantContext.attributes.default_locale\n", "metadata": {"path": "docs/developer/locale-resolution.md"}} {"id": "file-docs-developer-defining-doctypes.md", "type": "doc", "title": "Document: docs/developer/defining-doctypes.md", "content": "---\ntitle: Defining DocTypes\nsidebar_position: 2\n---\n\n# Defining DocTypes\n\nAdvanced patterns for defining DocTypes in Framework M.\n\n## Introduction to DocTypes\n\nIn Framework M, a **DocType** is both a data schema (Pydantic model) and a database table definition. All DocTypes should inherit from `DocType` (an alias for `BaseDocType`).\n\n```python\nfrom framework_m import DocType, Field\n\nclass MyDocType(DocType):\n \"\"\"DocType definition.\"\"\"\n # fields...\n```\n\n## Field Patterns\n\n### Required vs Optional\n\n```python\n# Required - no default\nname: str = Field(description=\"Customer name\")\n\n# Optional - with default\nemail: str = Field(default=\"\", description=\"Email\")\n\n# Nullable optional\nphone: str | None = Field(default=None, description=\"Phone\")\n```\n\n### Unique Fields\n\nTo enforce a unique constraint at the database level:\n\n```python\n# Email must be unique\nemail: str = Field(json_schema_extra={\"unique\": True})\n\n# Code must be unique\ncode: str = Field(min_length=3, json_schema_extra={\"unique\": True})\n```\n\n### Field Validation\n\n```python\nfrom framework_m import Field\n\n# String constraints\ncode: str = Field(min_length=3, max_length=20)\n\n# Number constraints\nquantity: int = Field(ge=0, le=1000) # >= 0, <= 1000\nprice: Decimal = Field(gt=0) # > 0\n\n# Pattern matching\nemail: str = Field(pattern=r\"^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$\")\n```\n\n### Common Types\n\n```python\nfrom datetime import date, datetime\nfrom decimal import Decimal\nfrom uuid import UUID\n\n# Dates\ndue_date: date = Field(description=\"Due date\")\ncreated_at: datetime = Field(description=\"Created timestamp\")\n\n# Money\namount: Decimal = Field(decimal_places=2, description=\"Amount\")\n\n# References\nrelated_id: UUID = Field(description=\"Related document ID\")\n```\n\n## Child Tables\n\nFor one-to-many relationships:\n\n```python\nclass InvoiceItem(DocType):\n \"\"\"Invoice line item.\"\"\"\n\n __doctype_name__: ClassVar[str] = \"InvoiceItem\"\n\n item_code: str = Field(description=\"Item code\")\n quantity: int = Field(default=1, ge=1)\n rate: Decimal = Field(description=\"Unit price\")\n amount: Decimal = Field(default=Decimal(\"0\"))\n\n class Meta:\n is_child_table: ClassVar[bool] = True\n\n\nclass Invoice(DocType):\n \"\"\"Invoice with line items.\"\"\"\n\n __doctype_name__: ClassVar[str] = \"Invoice\"\n\n customer: str = Field(description=\"Customer name\")\n items: list[InvoiceItem] = Field(default_factory=list)\n total: Decimal = Field(default=Decimal(\"0\"))\n```\n\nCalculate totals in controller:\n\n```python\nfrom framework_m import Controller\n\nclass InvoiceController(Controller[Invoice]):\n async def before_save(self, context=None):\n for item in self.doc.items:\n item.amount = item.quantity * item.rate\n self.doc.total = sum(i.amount for i in self.doc.items)\n```\n\n## Meta Options\n\n```python\nclass Invoice(DocType):\n # ... fields ...\n\n class Meta:\n # Naming\n naming_rule: ClassVar[str] = \"autoincrement\"\n # Options: \"autoincrement\", \"uuid\", \"field:customer\"\n\n # Behavior\n is_submittable: ClassVar[bool] = True\n is_child_table: ClassVar[bool] = False\n\n # API exposure\n api_resource: ClassVar[bool] = True\n\n # Database\n table_name: ClassVar[str] = \"invoice\" # Custom table name\n\n # Permissions\n permissions: ClassVar[dict] = {\n \"read\": [\"Employee\", \"Manager\"],\n \"write\": [\"Manager\"],\n \"submit\": [\"Manager\"],\n \"delete\": [\"Administrator\"],\n }\n```\n\n## Submittable Documents\n\nFor documents with approval workflow:\n\n```python\nclass PurchaseOrder(DocType):\n __doctype_name__: ClassVar[str] = \"PurchaseOrder\"\n\n supplier: str = Field(description=\"Supplier\")\n total: Decimal = Field(description=\"Total amount\")\n docstatus: int = Field(default=0) # 0=Draft, 1=Submitted, 2=Cancelled\n\n class Meta:\n is_submittable: ClassVar[bool] = True\n```\n\nController hooks for submission:\n\n```python\nfrom framework_m import Controller\n\nclass PurchaseOrderController(Controller[PurchaseOrder]):\n async def on_submit(self, context=None):\n \"\"\"Called when document is submitted.\"\"\"\n # Create ledger entries, reserve inventory, etc.\n if self.doc.total > 10000:\n raise ValueError(\"Orders over 10000 require approval\")\n\n async def on_cancel(self, context=None):\n \"\"\"Called when document is cancelled.\"\"\"\n # Reverse ledger entries, release inventory, etc.\n pass\n```\n\n## Custom Validators\n\nFor complex validation:\n\n```python\nfrom pydantic import field_validator, model_validator\n\nclass Order(DocType):\n start_date: date\n end_date: date\n quantity: int\n\n @field_validator(\"quantity\")\n @classmethod\n def validate_quantity(cls, v):\n if v <= 0:\n raise ValueError(\"Quantity must be positive\")\n return v\n\n @model_validator(mode=\"after\")\n def validate_dates(self):\n if self.end_date < self.start_date:\n raise ValueError(\"End date must be after start date\")\n return self\n```\n\n## Database Indexes\n\nFor query performance:\n\n```python\nclass Meta:\n indexes: ClassVar[list] = [\n (\"status\",), # Single column\n (\"customer\", \"date\"), # Composite\n (\"owner\", \"creation\"), # For RLS queries\n ]\n```\n\n## Linking DocTypes\n\nReference other DocTypes:\n\n```python\nclass Invoice(DocType):\n # Simple reference (stores ID)\n customer_id: UUID = Field(description=\"Customer\")\n\n # Reference with name (for display)\n customer_name: str = Field(default=\"\", description=\"Customer name\")\n```\n\nIn controller, fetch related data:\n\n```python\nasync def before_save(self, context=None):\n if self.doc.customer_id:\n customer = await customer_repo.get(self.doc.customer_id)\n if customer:\n self.doc.customer_name = customer.name\n```\n\n## Next Steps\n\n- [Creating Apps](./creating-apps.md) - Build complete applications\n- [Architecture](./architecture.md) - System overview\n", "metadata": {"path": "docs/developer/defining-doctypes.md"}} {"id": "file-docs-developer-migrations.md", "type": "doc", "title": "Document: docs/developer/migrations.md", "content": "# Database Migrations\n\nFramework M uses **Alembic** to manage database migrations. It supports both automatic discovery of DocTypes and selective migration based on \"installed apps\".\n\n## Migration Anatomy\nMigrations are stored in the host application's directory (e.g., `apps/business-m/alembic/`).\n\n- `alembic/env.py`: Orchestrates how DocTypes are discovered and how MetaData is built.\n- `alembic/versions/`: Contains the generated migration scripts.\n- `alembic.ini`: Configuration file for Alembic.\n\n## Discovery Strategies\n\nFramework M supports two modes of DocType discovery for migrations:\n\n### 1. Selective Discovery (Recommended for Production)\nThis mode mimics Frappe's behavior. Migrations ONLY manage DocTypes from apps specified in the `INSTALLED_APPS` list.\n\n**How to trigger:**\n- Use the `--apps` flag in the CLI:\n ```bash\n m migrate create \"added finance\" --apps \"finance,m_imprest\"\n ```\n- Or set the `INSTALLED_APPS` environment variable:\n ```bash\n export INSTALLED_APPS=\"finance,wms\"\n m migrate status\n ```\n\n**How it works:**\nThe `env.py` script calls `MetaRegistry.load_apps(apps)`, which imports the specified packages and registers their DocTypes. Tables not in these apps are ignored by the migration generator.\n\n### 2. Monolithic Discovery (Development Default)\nIf no `INSTALLED_APPS` or `--apps` are provided, the system falls back to a recursive filesystem scan of the `src/` folder.\n\n**How it works:**\nThe system crawls all directories named `doctypes` within the workspace and automatically registers every DocType it finds.\n\n## Sync vs. Migrate (The Dual-Track Approach)\n\nTo provide a Frappe-like \"it just works\" experience while maintaining production-grade control, Framework M uses a dual-track strategy:\n\n### 1. The \"Sync\" Track (Declarative / Frappe-style)\nUse `m migrate sync` when you want the database to exactly match your current DocType definitions.\n\n- **Behavior**: Compares current DocTypes with the DB schema and applies `CREATE TABLE` / `ALTER TABLE` immediately.\n- **Self-Contained**: No migration files are created. It is entirely driven by code.\n- **Use Case**: Rapid development, local prototyping, and \"Syncing\" schema stability.\n\n### 1.1 Sync Hooks (The Lifecycle Track)\nTo handle complex data transitions during a `sync`, each app can define hooks in its `migrations` module:\n\n**File: `libs/wms/src/wms/migrations.py`**\n```python\ndef before_sync(engine):\n \"\"\"Run before DDL sync (e.g. drop old constraints)\"\"\"\n pass\n\ndef after_sync(engine):\n \"\"\"Run after DDL sync (e.g. initialize default data)\"\"\"\n pass\n```\n\n- **Execution**: `m migrate sync` automatically discovers these hooks in all apps listed in `--apps`.\n- **Context**: The hooks receive a SQLAlchemy `engine` for direct data manipulation.\n\n### 2. The \"Migrate\" Track (Imperative / Alembic)\nUse the standard commands (`create`, `run`) when you need to ship **ordered patches** or logic-heavy transitions (data migrations, complex index changes).\n\n- **Behavior**: Uses version files inside each app's `alembic/versions` folder.\n- **Self-Contained**: Each app maintains its own version history.\n- **Use Case**: Production rollouts, data migrations, and multi-tenant patches.\n\n```bash\nm migrate create \"v1 patch\" --app \"wms\"\nm migrate\n```\n\n## Comparisons with Frappe\n\n| Concept | Frappe | Framework M |\n|---------|--------|-------------|\n| **Auto-Sync** | `bench migrate` (sync phase) | `m migrate sync` |\n| **Manual Patches** | `patches.txt` + Python scripts | `m migrate run` + Alembic scripts |\n| **Modularity** | Distributed `patches.txt` in each app | Distributed `alembic/versions` in each app |\n| **Dependencies** | Handled via `after_install` or global order | Handled via Alembic's `depends_on` |\n\n## Commands\n\n| Command | Description |\n|---------|-------------|\n| `m migrate all` | **(Recommended)** Runs both versioned patches (`run`) and declarative sync. |\n| `m migrate sync` | Direct DocType -> DB synchronization (no files). |\n| `m migrate init` | Initialize Alembic for versioned migrations. |\n| `m migrate create \"msg\" [--app ...]` | Generate a versioned patch for a specific app. |\n| `m migrate` | Alias for `m migrate run`. Runs all pending versioned patches. |\n| `m migrate status` | Show current database revision and pending status. |\n\n## Zero-Downtime Deployment Strategy (DevOps)\n\nBy decoupling `sync` and `run`, Framework M enables advanced zero-downtime workflows:\n\n1. **Stage 1: Safe Sync** - Run `m migrate sync` (declarative) before updating application code. This adds new nullable columns or tables that don't break existing code.\n2. **Stage 2: Code Rollout** - Update application instances to the new version.\n3. **Stage 3: Data Patches** - Run `m migrate run` (imperative) to perform data transformations or breaking schema changes once the new code is fully operational.\n\n## Tips for Monorepos\nIn a monorepo, it is best to run migrations from the **main app** (`apps/business-m`) but specify which **libs** should be included in the schema using the `--apps` flag.\n", "metadata": {"path": "docs/developer/migrations.md"}} {"id": "file-docs-developer-plugin-menu-code-examples.md", "type": "doc", "title": "Document: docs/developer/plugin-menu-code-examples.md", "content": "# Plugin Menu Implementation - Code Examples\n\n> **Prerequisites:** Read [Plugin Architecture Implementation](./plugin-architecture-implementation.md) first\n\n## Complete Working Example\n\nThis document shows the **exact code** you'll write to implement plugin menus.\n\n---\n\n## Phase 1: Build @framework-m/plugin-sdk Package\n\n### Directory Structure\n\n```\nlibs/framework-m-plugin-sdk/\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 tsconfig.json\n\u251c\u2500\u2500 vite.config.ts\n\u2514\u2500\u2500 src/\n \u251c\u2500\u2500 index.ts # Public API exports\n \u251c\u2500\u2500 types/\n \u2502 \u2514\u2500\u2500 plugin.ts # TypeScript interfaces\n \u251c\u2500\u2500 core/\n \u2502 \u251c\u2500\u2500 PluginRegistry.ts # Central registry\n \u2502 \u2514\u2500\u2500 ServiceContainer.ts # DI container\n \u251c\u2500\u2500 hooks/\n \u2502 \u251c\u2500\u2500 usePluginMenu.ts # React hook for menus\n \u2502 \u251c\u2500\u2500 usePlugin.ts # React hook for plugin data\n \u2502 \u2514\u2500\u2500 useService.ts # React hook for services\n \u2514\u2500\u2500 context/\n \u2514\u2500\u2500 PluginRegistryContext.tsx # React context provider\n```\n\n### 1. package.json\n\n```json\n{\n \"name\": \"@framework-m/plugin-sdk\",\n \"version\": \"0.1.0\",\n \"description\": \"Plugin system SDK for Framework M multi-app projects\",\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n }\n },\n \"files\": [\"dist\", \"README.md\"],\n \"scripts\": {\n \"build\": \"tsc && vite build\",\n \"dev\": \"vite build --watch\",\n \"type-check\": \"tsc --noEmit\"\n },\n \"keywords\": [\"framework-m\", \"plugin\", \"sdk\", \"multi-app\"],\n \"peerDependencies\": {\n \"react\": \"^18.0.0 || ^19.0.0\",\n \"react-dom\": \"^18.0.0 || ^19.0.0\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^19.1.0\",\n \"@types/react-dom\": \"^19.1.0\",\n \"typescript\": \"^5.8.0\",\n \"vite\": \"^6.3.0\",\n \"react\": \"^19.2.0\",\n \"react-dom\": \"^19.2.0\"\n }\n}\n```\n\n### 2. src/types/plugin.ts\n\n```typescript\nimport { RouteObject } from \"react-router-dom\";\n\n/**\n * Menu item representation\n */\nexport interface MenuItem {\n /** Unique identifier (e.g., \"sales.invoice\") */\n name: string;\n\n /** Display label (e.g., \"Sales Invoice\") */\n label: string;\n\n /** Route path (e.g., \"/doctypes/sales.invoice\") */\n route: string;\n\n /** Icon name from lucide-react (e.g., \"file-text\") */\n icon?: string;\n\n /** Module grouping (e.g., \"Sales\", \"Inventory\") */\n module?: string;\n\n /** Category within module (e.g., \"Transactions\", \"Masters\") */\n category?: string;\n\n /** Sort order (lower = higher priority) */\n order?: number;\n\n /** Nested menu items */\n children?: MenuItem[];\n\n /** Badge count (e.g., notification count) */\n badge?: number;\n\n /** Whether item is visible */\n hidden?: boolean;\n}\n\n/**\n * Service factory function\n */\nexport type ServiceFactory = () => Promise | T;\n\n/**\n * Provider component\n */\nexport interface Provider {\n /** Component factory (lazy loaded) */\n component: () => Promise<{ default: React.ComponentType }>;\n\n /** Props to pass to provider */\n props?: Record;\n}\n\n/**\n * DocType extension\n */\nexport interface DocTypeExtension {\n /** DocType name to extend */\n doctype: string;\n\n /** Custom list component */\n listComponent?: () => Promise<{ default: React.ComponentType }>;\n\n /** Custom form component */\n formComponent?: () => Promise<{ default: React.ComponentType }>;\n\n /** Custom show component */\n showComponent?: () => Promise<{ default: React.ComponentType }>;\n\n /** Additional fields */\n fields?: Array<{\n name: string;\n label: string;\n type: string;\n }>;\n}\n\n/**\n * Dashboard widget\n */\nexport interface Widget {\n /** Widget ID */\n id: string;\n\n /** Widget title */\n title: string;\n\n /** Component factory */\n component: () => Promise<{ default: React.ComponentType }>;\n\n /** Grid position */\n position?: {\n x: number;\n y: number;\n w: number;\n h: number;\n };\n}\n\n/**\n * Plugin manifest\n */\nexport interface FrameworkMPlugin {\n /** Plugin name (unique identifier) */\n name: string;\n\n /** Semantic version */\n version: string;\n\n /** Human-readable description */\n description?: string;\n\n /** Menu items contributed by this plugin */\n menu?: MenuItem[];\n\n /** Routes contributed by this plugin */\n routes?: RouteObject[];\n\n /** Services provided by this plugin */\n services?: Record;\n\n /** Provider components */\n providers?: Provider[];\n\n /** DocType extensions */\n doctypes?: Record;\n\n /** Dashboard widgets */\n widgets?: Widget[];\n\n /** Plugin dependencies */\n dependencies?: string[];\n\n /** Plugin initialization function */\n onInit?: () => Promise | void;\n\n /** Plugin cleanup function */\n onDestroy?: () => Promise | void;\n}\n```\n\n### 3. src/core/PluginRegistry.ts\n\n```typescript\nimport { FrameworkMPlugin, MenuItem } from \"../types/plugin\";\nimport { RouteObject } from \"react-router-dom\";\n\n/**\n * Central registry for all plugins\n */\nexport class PluginRegistry {\n private plugins = new Map();\n private menuCache: MenuItem[] | null = null;\n private routesCache: RouteObject[] | null = null;\n\n /**\n * Register a plugin\n */\n async register(plugin: FrameworkMPlugin): Promise {\n // Validation\n if (!plugin.name) {\n throw new Error(\"Plugin must have a name\");\n }\n\n if (!plugin.version) {\n throw new Error(\"Plugin must have a version\");\n }\n\n // Check dependencies\n if (plugin.dependencies) {\n for (const dep of plugin.dependencies) {\n if (!this.plugins.has(dep)) {\n console.warn(\n `Plugin ${plugin.name} depends on ${dep} which is not registered`,\n );\n }\n }\n }\n\n // Register\n if (this.plugins.has(plugin.name)) {\n console.warn(`Plugin ${plugin.name} already registered, overwriting`);\n }\n\n this.plugins.set(plugin.name, plugin);\n\n // Invalidate caches\n this.menuCache = null;\n this.routesCache = null;\n\n // Call onInit if provided\n if (plugin.onInit) {\n await plugin.onInit();\n }\n\n console.log(`\u2713 Registered plugin: ${plugin.name}@${plugin.version}`);\n }\n\n /**\n * Get merged menu tree from all plugins\n */\n getMenu(): MenuItem[] {\n if (this.menuCache) {\n return this.menuCache;\n }\n\n const allMenuItems: MenuItem[] = [];\n\n // Collect all menu items\n for (const plugin of this.plugins.values()) {\n if (plugin.menu) {\n allMenuItems.push(...plugin.menu);\n }\n }\n\n // Build hierarchical menu\n this.menuCache = this.buildMenuTree(allMenuItems);\n return this.menuCache;\n }\n\n /**\n * Build hierarchical menu tree grouped by module and category\n */\n private buildMenuTree(items: MenuItem[]): MenuItem[] {\n const moduleMap = new Map();\n\n for (const item of items) {\n // Skip hidden items\n if (item.hidden) continue;\n\n const moduleName = item.module || \"Other\";\n const moduleKey = moduleName.toLowerCase();\n\n // Get or create module group\n if (!moduleMap.has(moduleKey)) {\n moduleMap.set(moduleKey, {\n name: moduleKey,\n label: moduleName,\n route: `/${moduleKey}`,\n icon: this.getModuleIcon(moduleName),\n order: item.order,\n children: [],\n });\n }\n\n const moduleGroup = moduleMap.get(moduleKey)!;\n\n if (item.category) {\n // Find or create category subgroup\n const categoryKey = item.category.toLowerCase();\n let categoryGroup = moduleGroup.children?.find(\n c => c.name === categoryKey,\n );\n\n if (!categoryGroup) {\n categoryGroup = {\n name: categoryKey,\n label: item.category,\n route: `/${moduleKey}/${categoryKey}`,\n children: [],\n };\n moduleGroup.children!.push(categoryGroup);\n }\n\n // Add item to category\n categoryGroup.children!.push(item);\n } else {\n // No category, add directly to module\n moduleGroup.children!.push(item);\n }\n }\n\n // Convert to array and sort\n const modules = Array.from(moduleMap.values());\n\n // Sort modules by order\n modules.sort((a, b) => (a.order || 999) - (b.order || 999));\n\n // Sort children within each module\n for (const module of modules) {\n if (module.children) {\n // Sort categories\n module.children.sort((a, b) => (a.order || 999) - (b.order || 999));\n\n // Sort items within categories\n for (const category of module.children) {\n if (category.children) {\n category.children.sort(\n (a, b) => (a.order || 999) - (b.order || 999),\n );\n }\n }\n }\n }\n\n return modules;\n }\n\n /**\n * Get icon for module\n */\n private getModuleIcon(moduleName: string): string {\n const icons: Record = {\n Sales: \"shopping-cart\",\n Inventory: \"package\",\n HR: \"users\",\n Personnel: \"users\",\n Finance: \"dollar-sign\",\n Accounting: \"calculator\",\n Core: \"settings\",\n Settings: \"settings\",\n Warehouse: \"warehouse\",\n WMS: \"warehouse\",\n };\n\n return icons[moduleName] || \"folder\";\n }\n\n /**\n * Get all routes from all plugins\n */\n getRoutes(): RouteObject[] {\n if (this.routesCache) {\n return this.routesCache;\n }\n\n const allRoutes: RouteObject[] = [];\n\n for (const plugin of this.plugins.values()) {\n if (plugin.routes) {\n allRoutes.push(...plugin.routes);\n }\n }\n\n this.routesCache = allRoutes;\n return allRoutes;\n }\n\n /**\n * Get specific plugin by name\n */\n getPlugin(name: string): FrameworkMPlugin | undefined {\n return this.plugins.get(name);\n }\n\n /**\n * Get all registered plugins\n */\n getAllPlugins(): FrameworkMPlugin[] {\n return Array.from(this.plugins.values());\n }\n\n /**\n * Unregister a plugin\n */\n async unregister(name: string): Promise {\n const plugin = this.plugins.get(name);\n\n if (!plugin) {\n console.warn(`Plugin ${name} not found`);\n return;\n }\n\n // Call onDestroy if provided\n if (plugin.onDestroy) {\n await plugin.onDestroy();\n }\n\n this.plugins.delete(name);\n\n // Invalidate caches\n this.menuCache = null;\n this.routesCache = null;\n\n console.log(`\u2713 Unregistered plugin: ${name}`);\n }\n\n /**\n * Clear all plugins\n */\n async clear(): Promise {\n const plugins = Array.from(this.plugins.values());\n\n for (const plugin of plugins) {\n await this.unregister(plugin.name);\n }\n }\n}\n```\n\n### 4. src/context/PluginRegistryContext.tsx\n\n```typescript\nimport { createContext, ReactNode, useEffect, useState } from 'react';\nimport { PluginRegistry } from '../core/PluginRegistry';\n\nexport const PluginRegistryContext = createContext(null);\n\ninterface PluginRegistryProviderProps {\n children: ReactNode;\n registry?: PluginRegistry;\n}\n\nexport function PluginRegistryProvider({\n children,\n registry: externalRegistry,\n}: PluginRegistryProviderProps) {\n const [registry] = useState(() => externalRegistry || new PluginRegistry());\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n setIsReady(true);\n }, []);\n\n if (!isReady) {\n return (\n
\n Loading plugins...\n
\n );\n }\n\n return (\n \n {children}\n \n );\n}\n```\n\n### 5. src/hooks/usePluginMenu.ts\n\n```typescript\nimport { useContext, useMemo } from \"react\";\nimport { PluginRegistryContext } from \"../context/PluginRegistryContext\";\nimport { MenuItem } from \"../types/plugin\";\n\n/**\n * Hook to access the merged menu tree from all plugins\n */\nexport function usePluginMenu(): MenuItem[] {\n const registry = useContext(PluginRegistryContext);\n\n if (!registry) {\n throw new Error(\"usePluginMenu must be used within PluginRegistryProvider\");\n }\n\n return useMemo(() => registry.getMenu(), [registry]);\n}\n```\n\n### 6. src/hooks/usePlugin.ts\n\n```typescript\nimport { useContext, useMemo } from \"react\";\nimport { PluginRegistryContext } from \"../context/PluginRegistryContext\";\nimport { FrameworkMPlugin } from \"../types/plugin\";\n\n/**\n * Hook to access a specific plugin by name\n */\nexport function usePlugin(name: string): FrameworkMPlugin | undefined {\n const registry = useContext(PluginRegistryContext);\n\n if (!registry) {\n throw new Error(\"usePlugin must be used within PluginRegistryProvider\");\n }\n\n return useMemo(() => registry.getPlugin(name), [registry, name]);\n}\n```\n\n### 7. src/index.ts (Public API)\n\n```typescript\n// Types\nexport type {\n MenuItem,\n FrameworkMPlugin,\n ServiceFactory,\n Provider,\n DocTypeExtension,\n Widget,\n} from \"./types/plugin\";\n\n// Core\nexport { PluginRegistry } from \"./core/PluginRegistry\";\n\n// Context\nexport {\n PluginRegistryContext,\n PluginRegistryProvider,\n} from \"./context/PluginRegistryContext\";\n\n// Hooks\nexport { usePluginMenu } from \"./hooks/usePluginMenu\";\nexport { usePlugin } from \"./hooks/usePlugin\";\n```\n\n---\n\n## Phase 2: Update Shell App\n\n### frontend/package.json\n\n```json\n{\n \"name\": \"frontend\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"@framework-m/desk\": \"^0.1.0\",\n \"@framework-m/plugin-sdk\": \"^0.1.0\",\n \"@refinedev/core\": \"^5.0.8\",\n \"react\": \"^19.1.0\",\n \"react-dom\": \"^19.1.0\",\n \"react-router-dom\": \"^7.12.0\"\n }\n}\n```\n\n### frontend/src/App.tsx\n\n```typescript\nimport { PluginRegistry, PluginRegistryProvider } from '@framework-m/plugin-sdk';\nimport { useEffect, useState } from 'react';\nimport { BrowserRouter, Routes, Route } from 'react-router-dom';\nimport { Layout } from './layout/Layout';\n\n// For now, manually import plugins\n// Later: @framework-m/vite-plugin will auto-generate this\nimport wmsPlugin from '../../apps/wms/frontend/dist/plugin.config';\nimport personnelPlugin from '../../apps/personnel/frontend/dist/plugin.config';\n\nconst plugins = [wmsPlugin, personnelPlugin];\n\nasync function bootstrap() {\n const registry = new PluginRegistry();\n\n // Register all plugins\n for (const plugin of plugins) {\n await registry.register(plugin);\n }\n\n console.log('\u2713 All plugins registered');\n return registry;\n}\n\nexport function App() {\n const [registry, setRegistry] = useState(null);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n bootstrap()\n .then(setRegistry)\n .catch(setError);\n }, []);\n\n if (error) {\n return (\n
\n

Plugin Error

\n
{error.message}
\n
\n );\n }\n\n if (!registry) {\n return (\n
\n Loading plugins...\n
\n );\n }\n\n return (\n \n \n \n }>\n {/* Dynamic routes from plugins */}\n {registry.getRoutes().map((route, i) => (\n \n ))}\n \n \n \n \n );\n}\n```\n\n### frontend/src/layout/Sidebar.tsx\n\n```typescript\nimport { usePluginMenu } from '@framework-m/plugin-sdk';\nimport { useMemo, useState } from 'react';\nimport { useNavigate, useLocation } from 'react-router-dom';\nimport { Icon } from '../components/Icon';\n\nexport function Sidebar() {\n const navigate = useNavigate();\n const location = useLocation();\n const pluginMenu = usePluginMenu();\n const [collapsedGroups, setCollapsedGroups] = useState>({});\n\n // Group menu by module\n const groupedMenu = useMemo(() => {\n const groups: Record = {};\n\n for (const moduleGroup of pluginMenu) {\n if (moduleGroup.children) {\n groups[moduleGroup.label] = moduleGroup.children;\n }\n }\n\n return groups;\n }, [pluginMenu]);\n\n const toggleGroup = (groupName: string) => {\n setCollapsedGroups((prev) => ({\n ...prev,\n [groupName]: !prev[groupName],\n }));\n };\n\n return (\n \n {/* Plugin Menus */}\n {Object.entries(groupedMenu).map(([moduleName, items]) => {\n const moduleInfo = pluginMenu.find((m) => m.label === moduleName);\n\n return (\n
\n {/* Module Header */}\n toggleGroup(moduleName)}\n style={{\n width: '100%',\n padding: '0.75rem 1rem',\n background: 'none',\n border: 'none',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n cursor: 'pointer',\n fontSize: '0.875rem',\n fontWeight: 600,\n color: '#374151',\n }}\n >\n \n {moduleInfo?.icon && }\n {moduleName}\n \n \n \n\n {/* Module Items */}\n {!collapsedGroups[moduleName] && (\n
\n {items.map((item) => (\n navigate(item.route)}\n style={{\n width: '100%',\n padding: '0.5rem 1rem 0.5rem 2.5rem',\n background: location.pathname === item.route ? '#e5e7eb' : 'none',\n border: 'none',\n borderLeft:\n location.pathname === item.route\n ? '3px solid #3b82f6'\n : '3px solid transparent',\n color: location.pathname === item.route ? '#1f2937' : '#6b7280',\n textAlign: 'left',\n cursor: 'pointer',\n fontSize: '0.875rem',\n display: 'flex',\n alignItems: 'center',\n gap: '0.5rem',\n }}\n >\n {item.icon && }\n {item.label}\n {item.badge && (\n \n {item.badge}\n \n )}\n \n ))}\n
\n )}\n
\n );\n })}\n \n );\n}\n```\n\n---\n\n## Phase 3: Create WMS Plugin\n\n### apps/wms/frontend/plugin.config.ts\n\n```typescript\nimport { FrameworkMPlugin } from \"@framework-m/plugin-sdk\";\n\nexport default {\n name: \"wms\",\n version: \"1.0.0\",\n description: \"Warehouse Management System\",\n\n menu: [\n {\n name: \"wms.dashboard\",\n label: \"WMS Dashboard\",\n route: \"/wms/dashboard\",\n icon: \"layout-dashboard\",\n module: \"Warehouse\",\n order: 1,\n },\n {\n name: \"wms.warehouse\",\n label: \"Warehouse\",\n route: \"/doctypes/wms.warehouse\",\n icon: \"warehouse\",\n module: \"Warehouse\",\n category: \"Masters\",\n order: 10,\n },\n {\n name: \"wms.bin_location\",\n label: \"Bin Location\",\n route: \"/doctypes/wms.bin_location\",\n icon: \"map-pin\",\n module: \"Warehouse\",\n category: \"Masters\",\n order: 11,\n },\n {\n name: \"wms.stock_entry\",\n label: \"Stock Entry\",\n route: \"/doctypes/wms.stock_entry\",\n icon: \"package\",\n module: \"Warehouse\",\n category: \"Transactions\",\n order: 20,\n },\n {\n name: \"wms.stock_transfer\",\n label: \"Stock Transfer\",\n route: \"/doctypes/wms.stock_transfer\",\n icon: \"truck\",\n module: \"Warehouse\",\n category: \"Transactions\",\n order: 21,\n },\n ],\n\n routes: [\n {\n path: \"/wms/dashboard\",\n lazy: () => import(\"./pages/Dashboard\"),\n },\n {\n path: \"/wms/receiving\",\n lazy: () => import(\"./pages/Receiving\"),\n },\n {\n path: \"/wms/putaway\",\n lazy: () => import(\"./pages/Putaway\"),\n },\n ],\n\n onInit: async () => {\n console.log(\"\u2713 WMS Plugin initialized\");\n },\n} satisfies FrameworkMPlugin;\n```\n\n### apps/wms/frontend/package.json\n\n```json\n{\n \"name\": \"@my-company/wms\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"framework-m\": {\n \"plugin\": \"./dist/plugin.config.js\",\n \"type\": \"frontend-module\"\n },\n \"scripts\": {\n \"build\": \"tsc && vite build\",\n \"dev\": \"vite build --watch\"\n },\n \"dependencies\": {\n \"@framework-m/desk\": \"^0.1.0\",\n \"@framework-m/plugin-sdk\": \"^0.1.0\",\n \"react\": \"^19.1.0\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.8.0\",\n \"vite\": \"^6.3.0\"\n }\n}\n```\n\n---\n\n## Testing the Implementation\n\n### Step 1: Build Plugin SDK\n\n```bash\ncd libs/framework-m-plugin-sdk\npnpm install\npnpm build\n```\n\n### Step 2: Build WMS Plugin\n\n```bash\ncd apps/wms/frontend\npnpm install\npnpm build\n```\n\n### Step 3: Run Shell App\n\n```bash\ncd frontend\npnpm install\npnpm dev\n```\n\n### Expected Result\n\n```\n\u2713 Registered plugin: wms@1.0.0\n\u2713 WMS Plugin initialized\n\u2713 All plugins registered\n\nSidebar shows:\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 \ud83d\udce6 Warehouse \u2502\n\u2502 \u251c\u2500 Masters \u2502\n\u2502 \u2502 \u251c\u2500 Warehouse \u2502\n\u2502 \u2502 \u2514\u2500 Bin Location\u2502\n\u2502 \u2514\u2500 Transactions \u2502\n\u2502 \u251c\u2500 Stock Entry \u2502\n\u2502 \u2514\u2500 Stock Transfer\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n---\n\n## Next Steps\n\n1. **Add more plugins** - Create Personnel, Finance plugins\n2. **Build Vite plugin** - Auto-discover plugins instead of manual imports\n3. **Add favorites/recent** - Extend sidebar with user preferences\n4. **Add search** - Filter menu items with fuzzy search\n5. **Add tests** - Unit tests for PluginRegistry, integration tests for shell\n\n---\n\n## Common Issues & Solutions\n\n### Issue: \"usePluginMenu must be used within PluginRegistryProvider\"\n\n**Cause:** Sidebar used outside PluginRegistryProvider\n\n**Fix:** Wrap App in PluginRegistryProvider:\n\n```typescript\n\n \n {/* Now works */}\n \n\n```\n\n### Issue: Menu items not showing\n\n**Cause:** Plugin not registered or menu empty\n\n**Fix:** Check console for registration logs:\n\n```typescript\nconsole.log(\"Registered plugins:\", registry.getAllPlugins());\nconsole.log(\"Menu tree:\", registry.getMenu());\n```\n\n### Issue: Routes not working\n\n**Cause:** Routes not added to React Router\n\n**Fix:** Ensure routes from registry are rendered:\n\n```typescript\n{registry.getRoutes().map((route, i) => (\n \n))}\n```\n", "metadata": {"path": "docs/developer/plugin-menu-code-examples.md"}} {"id": "file-docs-developer-package-frontend-guide.md", "type": "doc", "title": "Document: docs/developer/package-frontend-guide.md", "content": "# Package Frontend Structure Guide\n\n## Overview\n\nFramework M supports multi-package UI composition, allowing Python packages to contribute frontend code that is automatically discovered and bundled during build or development.\n\n## Package Structure\n\nA Framework M package with frontend components follows this structure:\n\n```\nmy-package/\n\u251c\u2500\u2500 pyproject.toml # Package configuration\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 src/\n \u2514\u2500\u2500 my_package/\n \u251c\u2500\u2500 __init__.py\n \u251c\u2500\u2500 doctypes/ # Backend DocTypes\n \u2502 \u251c\u2500\u2500 __init__.py\n \u2502 \u2514\u2500\u2500 my_doctype.py\n \u2514\u2500\u2500 frontend/ # Frontend code\n \u251c\u2500\u2500 index.ts # Plugin entry point (required)\n \u251c\u2500\u2500 package.json # Frontend dependencies\n \u251c\u2500\u2500 tsconfig.json # TypeScript config\n \u251c\u2500\u2500 components/ # Shared components\n \u2502 \u251c\u2500\u2500 MyCard.tsx\n \u2502 \u2514\u2500\u2500 MyForm.tsx\n \u251c\u2500\u2500 pages/ # Custom pages\n \u2502 \u251c\u2500\u2500 MyList.tsx\n \u2502 \u2514\u2500\u2500 MyDetail.tsx\n \u2514\u2500\u2500 hooks/ # Custom React hooks\n \u2514\u2500\u2500 useMyData.ts\n```\n\n## Required Files\n\n### 1. pyproject.toml\n\nRegister both backend and frontend entry points:\n\n```toml\n[project]\nname = \"my-package\"\nversion = \"1.0.0\"\ndependencies = [\n \"framework-m>=0.1.0\",\n]\n\n# Backend entry point (for DocType discovery)\n[project.entry-points.\"framework_m.apps\"]\nmy_package = \"my_package:app\"\n\n# Frontend entry point (for UI discovery)\n[project.entry-points.\"framework_m.frontend\"]\nmy_package = \"my_package.frontend:plugin\"\n```\n\n### 2. frontend/index.ts\n\nThe main plugin entry point that registers your UI components:\n\n```typescript\nimport { registerPlugin } from \"@framework-m/core\";\nimport { MyCard } from \"./components/MyCard\";\nimport { MyForm } from \"./components/MyForm\";\nimport { MyListPage } from \"./pages/MyList\";\nimport { MyDetailPage } from \"./pages/MyDetail\";\n\n// Export plugin metadata\nexport const plugin = {\n name: \"my_package\",\n version: \"1.0.0\",\n};\n\n// Register plugin on import\nregisterPlugin({\n name: \"my_package\",\n version: \"1.0.0\",\n\n // Register custom pages\n pages: [\n {\n path: \"/app/my-doctype/list\",\n component: MyListPage,\n },\n {\n path: \"/app/my-doctype/:id\",\n component: MyDetailPage,\n },\n ],\n\n // Register components for use by other plugins\n components: {\n MyCard,\n MyForm,\n },\n\n // Inject into slots defined by other plugins\n slots: [\n {\n slot: \"desk.sidebar.bottom\",\n component: MyCard,\n priority: 10,\n },\n ],\n});\n```\n\n## Development Workflow\n\n### Local Development\n\nInstall your package in development mode and run the dev server:\n\n```bash\n# Install as editable package\npip install -e ./libs/my-package\n\n# Run dev server (auto-discovers all plugins)\nm dev\n\n# Your plugin UI is now available at http://localhost:5173\n```\n\nThe dev server will:\n\n- Discover your package via `framework_m.frontend` entry point\n- Generate Vite config with path aliases\n- Enable Hot Module Replacement (HMR) for your TypeScript/React code\n\n### Building for Production\n\n```bash\n# Build includes all discovered plugins\nm build --output ./dist\n\n# Result: Single optimized bundle with all plugins\n```\n\n## Frontend Dependencies\n\n### package.json\n\nEach package can declare its own frontend dependencies:\n\n```json\n{\n \"name\": \"@my-package/frontend\",\n \"version\": \"1.0.0\",\n \"peerDependencies\": {\n \"react\": \"^18.0.0\",\n \"react-dom\": \"^18.0.0\",\n \"@framework-m/core\": \"^0.1.0\"\n },\n \"dependencies\": {\n \"date-fns\": \"^2.30.0\",\n \"zustand\": \"^4.5.0\"\n },\n \"devDependencies\": {\n \"@types/react\": \"^18.0.0\",\n \"typescript\": \"^5.0.0\"\n }\n}\n```\n\n**Important:** Use `peerDependencies` for shared libraries (React, Framework M core) to avoid duplication in the final bundle.\n\n### TypeScript Configuration\n\nCreate a `tsconfig.json` for type checking:\n\n```json\n{\n \"extends\": \"@framework-m/tsconfig/base.json\",\n \"compilerOptions\": {\n \"outDir\": \"./dist\",\n \"rootDir\": \"./\",\n \"baseUrl\": \".\",\n \"paths\": {\n \"@framework-m/core\": [\"../../framework-m/frontend/src\"],\n \"@/*\": [\"./src/*\"]\n }\n },\n \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\", \"dist\"]\n}\n```\n\n## Import Patterns\n\n### Importing from Framework M Core\n\n```typescript\nimport { registerPlugin, useDocType, Button } from \"@framework-m/core\";\n```\n\n### Importing from Other Packages\n\n```typescript\n// Import components from business-m package\nimport { CustomerCard } from \"@business-m/frontend/components/CustomerCard\";\nimport { useInvoice } from \"@business-m/frontend/hooks/useInvoice\";\n```\n\nThe build system automatically resolves these imports via Vite path aliases.\n\n## Best Practices\n\n### 1. Component Isolation\n\nKeep components self-contained with their own styles:\n\n```typescript\n// components/MyCard.tsx\nimport { Card } from \"@framework-m/core\";\nimport styles from \"./MyCard.module.css\";\n\nexport function MyCard({ data }) {\n return (\n \n {/* Component content */}\n \n );\n}\n```\n\n### 2. Type Safety\n\nExport TypeScript types for components consumed by other packages:\n\n```typescript\n// types/index.ts\nexport interface MyCardProps {\n data: MyData;\n onAction?: (id: string) => void;\n}\n\n// components/MyCard.tsx\nimport type { MyCardProps } from \"../types\";\n\nexport function MyCard({ data, onAction }: MyCardProps) {\n // Implementation\n}\n```\n\n### 3. Tree-Shaking\n\nUse named exports to enable tree-shaking:\n\n```typescript\n// \u2705 Good: Named exports\nexport { MyCard } from \"./components/MyCard\";\nexport { MyForm } from \"./components/MyForm\";\n\n// \u274c Bad: Default exports\nexport default {\n MyCard,\n MyForm,\n};\n```\n\n### 4. Code Splitting\n\nUse dynamic imports for large components:\n\n```typescript\nimport { lazy } from \"react\";\n\nconst LargeReport = lazy(() => import(\"./components/LargeReport\"));\n\nregisterPlugin({\n name: \"my_package\",\n pages: [\n {\n path: \"/app/reports/large\",\n component: LargeReport, // Auto-code-split\n },\n ],\n});\n```\n\n## Testing\n\n### Unit Tests\n\nTest components in isolation:\n\n```typescript\n// components/__tests__/MyCard.test.tsx\nimport { render } from \"@testing-library/react\";\nimport { MyCard } from \"../MyCard\";\n\ndescribe(\"MyCard\", () => {\n it(\"renders data correctly\", () => {\n const { getByText } = render();\n expect(getByText(\"Test\")).toBeInTheDocument();\n });\n});\n```\n\n### Integration Tests\n\nTest plugin registration:\n\n```typescript\n// __tests__/plugin.test.ts\nimport { getRegisteredPlugins } from \"@framework-m/core\";\nimport { plugin } from \"../index\";\n\ndescribe(\"Plugin Registration\", () => {\n it(\"registers with correct metadata\", () => {\n const plugins = getRegisteredPlugins();\n const myPlugin = plugins.find(p => p.name === \"my_package\");\n\n expect(myPlugin).toBeDefined();\n expect(myPlugin.version).toBe(\"1.0.0\");\n });\n});\n```\n\n## Publishing\n\n### 1. Build the Package\n\n```bash\n# Ensure frontend code is included in package\npip install build\npython -m build\n```\n\n### 2. Verify Package Contents\n\n```bash\ntar -tzf dist/my_package-1.0.0.tar.gz | grep frontend\n# Should show:\n# my_package/frontend/index.ts\n# my_package/frontend/components/...\n# my_package/frontend/pages/...\n```\n\n### 3. Publish to PyPI\n\n```bash\npip install twine\ntwine upload dist/*\n```\n\n## Next Steps\n\n- [Plugin Composition Patterns](./plugin-composition-patterns.md) - Learn how to compose and override UI from multiple packages\n- [Migration Path](../adr/0010-multi-package-ui-composition.md#migration-path) - Migrate existing apps/ to packages\n- [Example: business-m Package](../examples/business-m-package.md) - Complete working example\n", "metadata": {"path": "docs/developer/package-frontend-guide.md"}} {"id": "file-docs-developer-plugin-package-architecture.md", "type": "doc", "title": "Document: docs/developer/plugin-package-architecture.md", "content": "# Plugin Package Architecture - Quick Reference\n\n## Visual Architecture Diagram\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 EXTERNAL DEPENDENCIES \u2502\n\u2502 @refinedev/*, react, react-router-dom, @tanstack/react-query \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 @framework-m/desk (CORE UI LIBRARY) \u2502\n\u2502 \ud83d\udce6 Published to GitLab npm: @framework-m/desk@0.1.0 \u2502\n\u2502 \ud83d\udcc1 Location: libs/framework-m-desk/ \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Responsibilities: \u2502\n\u2502 \u2022 Refine.dev data provider (REST API) \u2502\n\u2502 \u2022 Auth provider (JWT, sessions) \u2502\n\u2502 \u2022 Live provider (WebSocket) \u2502\n\u2502 \u2022 Base components: , , \u2502\n\u2502 \u2022 Hooks: useDocType(), useMetadata(), useWorkflow() \u2502\n\u2502 \u2022 Utilities: formatDate(), validateForm(), etc. \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Who uses it: \u2502\n\u2502 \u2713 Shell application (frontend/) \u2502\n\u2502 \u2713 All app plugins (apps/*/frontend/) \u2502\n\u2502 \u2713 Any Framework M project \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Breaking changes? NO - Existing users unaffected \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502\n \u25bc \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 @framework-m/plugin-sdk \u2502 \u2502 App Plugins (Workspace) \u2502\n\u2502 (PLUGIN INFRASTRUCTURE) \u2502 \u2502 NOT published to npm \u2502\n\u2502 \u2502 \u2502 \u2502\n\u2502 \ud83d\udce6 Published: GitLab npm \u2502 \u2502 Example: @my-company/wms \u2502\n\u2502 \ud83d\udcc1 libs/plugin-sdk/ \u2502 \u2502 \ud83d\udcc1 apps/wms/frontend/ \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Exports: \u2502 \u2502 Structure: \u2502\n\u2502 \u2022 PluginRegistry \u2502 \u2502 \u2022 plugin.config.ts \u2502\n\u2502 \u2022 ServiceContainer \u2502 \u2502 \u2022 package.json (framework-m) \u2502\n\u2502 \u2022 usePluginMenu() \u2502 \u2502 \u2022 src/pages/ \u2502\n\u2502 \u2022 useService() \u2502 \u2502 \u2022 src/components/ \u2502\n\u2502 \u2022 usePlugin() \u2502 \u2502 \u2022 src/services/ \u2502\n\u2502 \u2022 FrameworkMPlugin type \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2022 MenuItem type \u2502 \u2502 Defines: \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 \u2502 \u2022 Menu items \u2502\n\u2502 Used by: \u2502 \u2502 \u2022 Routes \u2502\n\u2502 \u2713 Shell app \u2502 \u2502 \u2022 Services \u2502\n\u2502 \u2713 Vite plugin \u2502 \u2502 \u2022 Providers \u2502\n\u2502 \u2713 App plugins \u2502 \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502 Examples: \u2502\n \u2502 \u2502 \u2022 apps/wms/frontend/ \u2502\n \u2502 \u2502 \u2022 apps/personnel/frontend/ \u2502\n \u2502 \u2502 \u2022 apps/finance/frontend/ \u2502\n \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502\n \u2502 \u2502\n \u25bc \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 @framework-m/vite-plugin \u2502 \u2502\n\u2502 (BUILD-TIME TOOLING) \u2502 \u2502\n\u2502 \u2502 \u2502\n\u2502 \ud83d\udce6 Published: GitLab npm \u2502 \u2502\n\u2502 \ud83d\udcc1 libs/vite-plugin/ \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 \u2502\n\u2502 Responsibilities: \u2502 \u2502\n\u2502 \u2022 Scan workspace for plugin packages \u2502 \u2502\n\u2502 \u2022 Generate virtual module \u2502 \u2502\n\u2502 \u2022 Configure code-splitting \u2502 \u2502\n\u2502 \u2022 Setup HMR for plugins \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524 \u2502\n\u2502 Used by: \u2502 \u2502\n\u2502 \u2713 Shell app (vite.config.ts only) \u2502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n \u2502 \u2502\n \u2502 discovers \u2502 imports\n \u2502 \u2502\n \u25bc \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 frontend/ (SHELL APPLICATION) \u2502\n\u2502 NOT published - Static build output \u2502\n\u2502 \ud83d\udcc1 frontend/ \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Responsibilities: \u2502\n\u2502 \u2022 Bootstrap PluginRegistry \u2502\n\u2502 \u2022 Compose all plugins into single UI \u2502\n\u2502 \u2022 Provide base layout (Sidebar, Header) \u2502\n\u2502 \u2022 Handle auth flow \u2502\n\u2502 \u2022 Error boundaries \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Dependencies: \u2502\n\u2502 \u2022 @framework-m/desk ^0.1.0 \u2502\n\u2502 \u2022 @framework-m/plugin-sdk ^0.1.0 \u2502\n\u2502 \u2022 @framework-m/vite-plugin ^0.1.0 (devDependency) \u2502\n\u2502 \u2022 Workspace plugins (apps/*/frontend/) via symlinks \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 Build Output (frontend/dist/): \u2502\n\u2502 \u2022 index.html \u2502\n\u2502 \u2022 assets/main-[hash].js (shell + framework) \u2502\n\u2502 \u2022 assets/wms-[hash].js (WMS plugin - lazy) \u2502\n\u2502 \u2022 assets/personnel-[hash].js (Personnel plugin - lazy) \u2502\n\u2502 \u2502\n\u2502 Deployed as: \u2502\n\u2502 \u2022 Python package (m build \u2192 pip install) \u2502\n\u2502 \u2022 CDN (aws s3 sync dist/ s3://bucket/) \u2502\n\u2502 \u2022 Container (COPY dist/ /app/static) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Data Flow: How Plugin Menus Work\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Step 1: Plugin Definition \u2502\n\u2502 apps/wms/frontend/plugin.config.ts \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 export default { \u2502\n\u2502 name: 'wms', \u2502\n\u2502 menu: [ \u2502\n\u2502 { \u2502\n\u2502 name: 'wms.warehouse', \u2502\n\u2502 label: 'Warehouse', \u2502\n\u2502 route: '/doctypes/wms.warehouse', \u2502\n\u2502 module: 'Inventory', \u2502\n\u2502 } \u2502\n\u2502 ] \u2502\n\u2502 } satisfies FrameworkMPlugin \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Step 2: Build-Time Discovery \u2502\n\u2502 @framework-m/vite-plugin scans workspace \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 1. Find all package.json files \u2502\n\u2502 2. Filter those with \"framework-m\" metadata \u2502\n\u2502 3. Collect plugin.config.ts paths \u2502\n\u2502 4. Generate virtual module: \u2502\n\u2502 \u2502\n\u2502 // virtual:framework-m-plugins \u2502\n\u2502 import wms from 'apps/wms/frontend/dist/plugin.config.js'; \u2502\n\u2502 import personnel from 'apps/personnel/.../plugin.config.js'; \u2502\n\u2502 export default [wms, personnel]; \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Step 3: Runtime Registration \u2502\n\u2502 frontend/src/App.tsx bootstrap() \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 import plugins from 'virtual:framework-m-plugins'; \u2502\n\u2502 \u2502\n\u2502 const registry = new PluginRegistry(); \u2502\n\u2502 for (const plugin of plugins) { \u2502\n\u2502 await registry.register(plugin); \u2502\n\u2502 } \u2502\n\u2502 \u2502\n\u2502 // Registry now contains: \u2502\n\u2502 // - wms.menu = [{name: 'wms.warehouse', ...}] \u2502\n\u2502 // - personnel.menu = [...] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Step 4: Menu Merging \u2502\n\u2502 PluginRegistry.getMenu() \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 1. Collect all menu items from all plugins \u2502\n\u2502 2. Group by module: \u2502\n\u2502 - Inventory: [wms.warehouse, wms.bin_location, ...] \u2502\n\u2502 - HR: [personnel.employee, ...] \u2502\n\u2502 3. Sort by order \u2502\n\u2502 4. Return merged tree: \u2502\n\u2502 \u2502\n\u2502 [ \u2502\n\u2502 { \u2502\n\u2502 name: 'inventory', \u2502\n\u2502 label: 'Inventory', \u2502\n\u2502 children: [ \u2502\n\u2502 { name: 'wms.warehouse', label: 'Warehouse' }, \u2502\n\u2502 { name: 'wms.bin_location', label: 'Bin Location' } \u2502\n\u2502 ] \u2502\n\u2502 }, \u2502\n\u2502 { name: 'hr', label: 'HR', children: [...] } \u2502\n\u2502 ] \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Step 5: UI Rendering \u2502\n\u2502 frontend/src/layout/Sidebar.tsx \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 function Sidebar() { \u2502\n\u2502 const pluginMenu = usePluginMenu(); // Hook from plugin-sdk \u2502\n\u2502 \u2502\n\u2502 return ( \u2502\n\u2502 \u2502\n\u2502 ) \u2502\n\u2502 } \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Package Dependency Chain\n\n```\nLevel 0 (External):\n react, react-dom, @refinedev/*, @tanstack/react-query\n \u2502\n \u25bc\nLevel 1 (Core):\n @framework-m/desk\n \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 \u2502\n \u25bc \u25bc\nLevel 2 (Plugin System):\n @framework-m/plugin-sdk App Plugins (@my-company/wms)\n \u2502 \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 \u2502\n \u25bc \u2502\nLevel 3 (Build Tools):\n @framework-m/vite-plugin \u2502\n \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\nLevel 4 (Shell):\n frontend/ (Shell App)\n \u2502\n \u25bc\nLevel 5 (Build Output):\n frontend/dist/ (Static Assets)\n```\n\n## Workspace Structure Example\n\n```\nmy-erp-project/\n\u251c\u2500\u2500 pnpm-workspace.yaml\n\u251c\u2500\u2500 package.json\n\u2502\n\u251c\u2500\u2500 frontend/ # Shell Application\n\u2502 \u251c\u2500\u2500 package.json\n\u2502 \u2502 \u2514\u2500\u2500 dependencies:\n\u2502 \u2502 \u251c\u2500\u2500 @framework-m/desk: ^0.1.0\n\u2502 \u2502 \u2514\u2500\u2500 @framework-m/plugin-sdk: ^0.1.0\n\u2502 \u251c\u2500\u2500 vite.config.ts\n\u2502 \u2502 \u2514\u2500\u2500 plugins: [frameworkMPlugin()]\n\u2502 \u251c\u2500\u2500 src/\n\u2502 \u2502 \u251c\u2500\u2500 App.tsx # Bootstrap plugins\n\u2502 \u2502 \u2514\u2500\u2500 layout/Sidebar.tsx # Uses usePluginMenu()\n\u2502 \u2514\u2500\u2500 dist/ # Build output\n\u2502\n\u251c\u2500\u2500 apps/\n\u2502 \u251c\u2500\u2500 wms/\n\u2502 \u2502 \u251c\u2500\u2500 backend/ # Python app\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 pyproject.toml\n\u2502 \u2502 \u2514\u2500\u2500 frontend/ # WMS Plugin\n\u2502 \u2502 \u251c\u2500\u2500 package.json\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 name: @my-company/wms\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 private: true\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 framework-m:\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 plugin: ./dist/plugin.config.js\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 dependencies:\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 @framework-m/desk: ^0.1.0\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 @framework-m/plugin-sdk: ^0.1.0\n\u2502 \u2502 \u251c\u2500\u2500 plugin.config.ts # Menu, routes, services\n\u2502 \u2502 \u2514\u2500\u2500 src/\n\u2502 \u2502 \u251c\u2500\u2500 pages/\n\u2502 \u2502 \u251c\u2500\u2500 components/\n\u2502 \u2502 \u2514\u2500\u2500 services/\n\u2502 \u2502\n\u2502 \u2514\u2500\u2500 personnel/\n\u2502 \u251c\u2500\u2500 backend/\n\u2502 \u2514\u2500\u2500 frontend/ # Personnel Plugin\n\u2502 \u251c\u2500\u2500 package.json\n\u2502 \u251c\u2500\u2500 plugin.config.ts\n\u2502 \u2514\u2500\u2500 src/\n\u2502\n\u2514\u2500\u2500 libs/ # Framework packages\n \u251c\u2500\u2500 framework-m-desk/ # Published to npm\n \u2502 \u251c\u2500\u2500 package.json\n \u2502 \u2502 \u2514\u2500\u2500 name: @framework-m/desk\n \u2502 \u2514\u2500\u2500 src/\n \u251c\u2500\u2500 framework-m-plugin-sdk/ # Published to npm\n \u2502 \u251c\u2500\u2500 package.json\n \u2502 \u2502 \u2514\u2500\u2500 name: @framework-m/plugin-sdk\n \u2502 \u2514\u2500\u2500 src/\n \u2514\u2500\u2500 framework-m-vite-plugin/ # Published to npm\n \u251c\u2500\u2500 package.json\n \u2502 \u2514\u2500\u2500 name: @framework-m/vite-plugin\n \u2514\u2500\u2500 src/\n```\n\n## Package Publishing Matrix\n\n| Package | Location | Published? | Registry | Versioning |\n| -------------------------- | ------------------------------- | ---------- | ---------- | -------------- |\n| `@framework-m/desk` | `libs/framework-m-desk/` | \u2705 Yes | GitLab npm | Follows Python |\n| `@framework-m/plugin-sdk` | `libs/framework-m-plugin-sdk/` | \u2705 Yes | GitLab npm | Independent |\n| `@framework-m/vite-plugin` | `libs/framework-m-vite-plugin/` | \u2705 Yes | GitLab npm | Independent |\n| `frontend/` (Shell) | `frontend/` | \u274c No | - | - |\n| `@my-company/wms` | `apps/wms/frontend/` | \u274c Never | Workspace | Private |\n| `@my-company/personnel` | `apps/personnel/frontend/` | \u274c Never | Workspace | Private |\n\n## When to Use Each Package\n\n### Use `@framework-m/desk` when:\n\n- \u2705 Building any Framework M frontend (single or multi-app)\n- \u2705 Need data provider for REST API\n- \u2705 Need auth provider\n- \u2705 Need base DocType components\n- \u2705 Want to use Framework M hooks\n\n### Use `@framework-m/plugin-sdk` when:\n\n- \u2705 Building multi-app project with plugins\n- \u2705 Need to access plugin menus/routes\n- \u2705 Want to register services\n- \u2705 Creating reusable app modules\n\n### Use `@framework-m/vite-plugin` when:\n\n- \u2705 Setting up shell app build\n- \u2705 Need auto-discovery of plugins\n- \u2705 Want code-splitting per plugin\n- \u2705 Building multi-app project\n\n### Don't use plugins if:\n\n- \u274c Single-app project (just use `@framework-m/desk` directly)\n- \u274c Simple customization (add pages directly to `frontend/src/`)\n- \u274c Prototyping (overhead not worth it)\n\n## Migration Path\n\n### Existing Single-App Projects (No Changes Needed)\n\n```json\n// Current setup - Still works!\n{\n \"name\": \"my-app-frontend\",\n \"dependencies\": {\n \"@refinedev/core\": \"^5.0.0\",\n \"react\": \"^19.0.0\"\n }\n}\n\n// Add desk when ready (optional)\n{\n \"dependencies\": {\n \"@framework-m/desk\": \"^0.1.0\", // \u2190 Just add this\n \"react\": \"^19.0.0\"\n }\n}\n```\n\n### New Multi-App Projects\n\n```json\n// Shell app\n{\n \"name\": \"frontend\",\n \"dependencies\": {\n \"@framework-m/desk\": \"^0.1.0\",\n \"@framework-m/plugin-sdk\": \"^0.1.0\"\n },\n \"devDependencies\": {\n \"@framework-m/vite-plugin\": \"^0.1.0\"\n }\n}\n\n// Plugin app (workspace package)\n{\n \"name\": \"@my-company/wms\",\n \"private\": true,\n \"framework-m\": {\n \"plugin\": \"./dist/plugin.config.js\"\n },\n \"dependencies\": {\n \"@framework-m/desk\": \"workspace:^0.1.0\",\n \"@framework-m/plugin-sdk\": \"workspace:^0.1.0\"\n }\n}\n```\n\n## Quick Decision Tree\n\n```\nAre you building a single app?\n\u251c\u2500 Yes \u2192 Just use @framework-m/desk\n\u2502 No plugin system needed\n\u2502\n\u2514\u2500 No (multi-app) \u2192 Use plugin system\n \u2502\n \u251c\u2500 Shell app needs:\n \u2502 \u251c\u2500 @framework-m/desk\n \u2502 \u251c\u2500 @framework-m/plugin-sdk\n \u2502 \u2514\u2500 @framework-m/vite-plugin (devDependency)\n \u2502\n \u2514\u2500 Plugin apps need:\n \u251c\u2500 @framework-m/desk\n \u2514\u2500 @framework-m/plugin-sdk\n```\n", "metadata": {"path": "docs/developer/plugin-package-architecture.md"}} {"id": "file-docs-developer-per-tenant-locales.md", "type": "doc", "title": "Document: docs/developer/per-tenant-locales.md", "content": "---\ntitle: Per-Tenant Locales\nsidebar_position: 5\n---\n\n# Per-Tenant Locales and Translation Overrides\nand override system translations to match their specific terminology,\nindustry language, or brand voice.\n\n## Overview\n\nFramework M supports tenant-specific localization through two mechanisms:\n\n1. **Tenant Default Locale**: Set organization-wide language preference\n2. **Tenant Translation Overrides**: Customize specific translations\n\n## Setting Tenant Default Locale\n\n### Backend: Configure Tenant\n\nSet the default locale in the tenant's attributes:\n\n```python\nfrom framework_m.core.interfaces.tenant import TenantContext\n\n# Configure tenant with Hindi as default locale\ntenant = TenantContext(\n tenant_id=\"acme-corp\",\n attributes={\n \"default_locale\": \"hi\", # Hindi\n \"plan\": \"enterprise\",\n \"features\": [\"advanced_reports\"]\n }\n)\n```\n\n### Locale Resolution Priority\n\nWhen a user makes a request, the locale is resolved in this order:\n\n1. **Accept-Language header** (browser preference)\n2. **User.locale field** (personal preference)\n3. **Tenant default_locale** (organization default) \u2190 Tenant setting\n4. **System DEFAULT_LOCALE** (fallback to \"en\")\n\n### Example: Healthcare Organization\n\n```python\n# Healthcare tenant defaults to Tamil\nhealthcare_tenant = TenantContext(\n tenant_id=\"healthcare-india\",\n attributes={\n \"default_locale\": \"ta\", # Tamil\n \"industry\": \"healthcare\"\n }\n)\n\n# All users in this tenant see Tamil by default\n# unless they:\n# - Set their browser to a different language\n# - Set their personal locale preference\n```\n\n## Tenant Translation Overrides\n\nTenants can provide custom translations to override system translations.\nThis is useful for:\n\n- Industry-specific terminology\n- Brand-specific wording\n- Localized company names in messages\n\n### Creating Tenant Translations\n\n#### Option 1: Via API\n\n```bash\n# Create tenant-specific translation\nPOST /api/v1/TenantTranslation\nAuthorization: Bearer \nX-Tenant-ID: acme-corp\n\n{\n \"tenant_id\": \"acme-corp\",\n \"source_text\": \"Customer\",\n \"translated_text\": \"Patient\",\n \"locale\": \"en\",\n \"context\": \"field_label\"\n}\n```\n\n#### Option 2: Via Python Code\n\n```python\nfrom framework_m.core.doctypes.tenant_translation import TenantTranslation\n\n# Healthcare tenant uses \"Patient\" instead of \"Customer\"\npatient_translation = TenantTranslation(\n tenant_id=\"healthcare-india\",\n source_text=\"Customer\",\n translated_text=\"Patient\",\n locale=\"en\",\n context=\"field_label\"\n)\n\n# Same in Tamil\npatient_translation_ta = TenantTranslation(\n tenant_id=\"healthcare-india\",\n source_text=\"Customer\",\n translated_text=\"\u0ba8\u0bcb\u0baf\u0bbe\u0bb3\u0bbf\", # Patient in Tamil\n locale=\"ta\",\n context=\"field_label\"\n)\n```\n\n### Translation Priority\n\nWhen translating text, the system checks in this order:\n\n1. **TenantTranslation** (tenant-specific override)\n2. **Translation** (system-wide translation)\n3. **Default parameter** (if provided)\n4. **Source text** (original text)\n\n### Example: Retail vs Healthcare\n\n```python\n# System translation (default)\nTranslation(\n source_text=\"Customer\",\n translated_text=\"\u0917\u094d\u0930\u093e\u0939\u0915\", # Customer in Hindi\n locale=\"hi\",\n context=\"field_label\"\n)\n\n# Retail tenant override (uses \"Client\")\nTenantTranslation(\n tenant_id=\"retail-corp\",\n source_text=\"Customer\",\n translated_text=\"\u0915\u094d\u0932\u093e\u0907\u0902\u091f\", # Client in Hindi\n locale=\"hi\",\n context=\"field_label\"\n)\n\n# Healthcare tenant override (uses \"Patient\")\nTenantTranslation(\n tenant_id=\"healthcare-india\",\n source_text=\"Customer\",\n translated_text=\"\u0930\u094b\u0917\u0940\", # Patient in Hindi\n locale=\"hi\",\n context=\"field_label\"\n)\n```\n\n### Result\n\nWhen each tenant's users see the \"Customer\" field:\n\n- **Retail tenant**: \"\u0915\u094d\u0932\u093e\u0907\u0902\u091f\" (Client)\n- **Healthcare tenant**: \"\u0930\u094b\u0917\u0940\" (Patient)\n- **Other tenants**: \"\u0917\u094d\u0930\u093e\u0939\u0915\" (Customer - system default)\n\n## Managing Tenant Translations\n\n### List Tenant Translations\n\n```bash\n# Get all translations for a tenant\nGET /api/v1/TenantTranslation?filters=[{\"field\":\"tenant_id\",\"operator\":\"eq\",\"value\":\"acme-corp\"}]\nAuthorization: Bearer \nX-Tenant-ID: acme-corp\n```\n\n### Update Tenant Translation\n\n```bash\n# Update existing translation\nPUT /api/v1/TenantTranslation/{id}\nAuthorization: Bearer \nX-Tenant-ID: acme-corp\n\n{\n \"translated_text\": \"Updated translation\"\n}\n```\n\n### Delete Tenant Translation\n\n```bash\n# Remove custom translation (falls back to system translation)\nDELETE /api/v1/TenantTranslation/{id}\nAuthorization: Bearer \nX-Tenant-ID: acme-corp\n```\n\n## Row-Level Security\n\nTenantTranslation DocType has RLS enabled:\n\n```python\nclass TenantTranslation(BaseDocType):\n class Meta:\n apply_rls = True\n rls_field = \"tenant_id\"\n```\n\nThis ensures:\n\n- Tenants can only see/modify their own translations\n- System automatically filters by tenant_id\n- No cross-tenant data leakage\n\n## Frontend Usage\n\n### Automatic Translation\n\nField labels in forms are automatically translated based on:\n\n1. User's resolved locale\n2. Tenant overrides (if any)\n\n```typescript\n// Frontend fetches metadata\nconst meta = await fetch('/api/meta/Invoice', {\n headers: {\n 'X-Tenant-ID': 'healthcare-india',\n 'Accept-Language': 'ta'\n }\n});\n\n// Response includes translated field labels\n{\n \"doctype\": \"Invoice\",\n \"locale\": \"ta\",\n \"schema\": {\n \"properties\": {\n \"customer\": {\n \"description\": \"\u0ba8\u0bcb\u0baf\u0bbe\u0bb3\u0bbf\", // \"Patient\" (tenant override)\n \"type\": \"string\"\n }\n }\n }\n}\n```\n\n### Translation Context\n\nUse context to disambiguate translations:\n\n```python\n# Button label\nTenantTranslation(\n tenant_id=\"acme-corp\",\n source_text=\"Save\",\n translated_text=\"\u0938\u0941\u0930\u0915\u094d\u0937\u093f\u0924 \u0915\u0930\u0947\u0902\", # \"Secure/Protect\" emphasis\n locale=\"hi\",\n context=\"button\"\n)\n\n# Field label\nTenantTranslation(\n tenant_id=\"acme-corp\",\n source_text=\"Save\",\n translated_text=\"\u092c\u091a\u0924\", # \"Savings\" (financial context)\n locale=\"hi\",\n context=\"field_label\"\n)\n```\n\n## Best Practices\n\n### 1. Start with System Translations\n\nOnly create tenant overrides when necessary:\n\n```python\n# Good: Override for industry-specific term\nTenantTranslation(\n tenant_id=\"medical-corp\",\n source_text=\"Customer\",\n translated_text=\"Patient\",\n locale=\"en\"\n)\n\n# Avoid: Duplicating system translations unnecessarily\n# (use system Translation instead)\n```\n\n### 2. Use Consistent Context\n\n```python\n# Consistent context for all field labels\ncontext=\"field_label\"\n\n# Consistent context for all buttons\ncontext=\"button\"\n\n# Consistent context for all messages\ncontext=\"message\"\n```\n\n### 3. Document Tenant Customizations\n\nKeep a record of why overrides exist:\n\n```python\n# Document the reason in comments or separate docs\nTenantTranslation(\n tenant_id=\"healthcare-india\",\n source_text=\"Total Amount\",\n translated_text=\"\u0bae\u0bca\u0ba4\u0bcd\u0ba4 \u0b95\u0b9f\u0bcd\u0b9f\u0ba3\u0bae\u0bcd\", # \"Total Fee\" per healthcare terminology\n locale=\"ta\",\n context=\"field_label\"\n)\n```\n\n### 4. Test in Multiple Locales\n\n```bash\n# Test each tenant's locale\ncurl -H \"X-Tenant-ID: healthcare-india\" \\\n -H \"Accept-Language: ta\" \\\n /api/meta/Invoice\n\ncurl -H \"X-Tenant-ID: retail-corp\" \\\n -H \"Accept-Language: hi\" \\\n /api/meta/Invoice\n```\n\n## Migration from Single-Tenant\n\nIf migrating from single-tenant to multi-tenant:\n\n1. **Review existing translations**: Decide which are tenant-specific\n2. **Create tenant translations**: Move tenant-specific translations to TenantTranslation\n3. **Keep system translations**: Keep common translations in Translation\n4. **Test thoroughly**: Verify each tenant sees correct translations\n\n## See Also\n\n- `framework_m.core.doctypes.tenant_translation` - TenantTranslation DocType\n- `framework_m.adapters.i18n.DefaultI18nAdapter` - Translation adapter with tenant support\n- `framework_m.adapters.web.middleware.locale` - Locale resolution middleware\n- `docs/developer/locale-resolution.md` - Complete locale resolution guide\n", "metadata": {"path": "docs/developer/per-tenant-locales.md"}} {"id": "file-docs-developer-features.md", "type": "doc", "title": "Document: docs/developer/features.md", "content": "---\ntitle: Features\nsidebar_position: 3\n---\n\n# Framework M Features\n\nAuto-generated feature list from phase checklists.\n\n## Overview\n\n| Phase | Title | Completion |\n| ----- | ------------------------------------------------------------------------------------------------------ | --------------- |\n| 01 | [Phase 01: Core Kernel & Interfaces](#phase-01-core-kernel--interfaces) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100% |\n| 02 | [Phase 02: DocType Engine & Database](#phase-02-doctype-engine--database) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100% |\n| 03 | [Phase 03: API Layer & Authorization](#phase-03-api-layer--authorization) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100% |\n| 04 | [Phase 04: Background Jobs & Events](#phase-04-background-jobs--events) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591 98% |\n| 05 | [Phase 05: CLI & Developer Tools](#phase-05-cli--developer-tools) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591 97% |\n| 06 | [Phase 06: Built-in DocTypes & Core Features](#phase-06-built-in-doctypes--core-features) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591 99% |\n| 07 | [Phase 07: Studio (Code Generation UI)](#phase-07-studio-code-generation-ui) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591 93% |\n| 08 | [Phase 08: Workflows & Advanced Features](#phase-08-workflows--advanced-features) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100% |\n| 09a | [Phase 09A: Frontend](#phase-09a-frontend) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591 97% |\n| 09b | [Phase 09B: Documentation](#phase-09b-documentation) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100% |\n| 10 | [Phase 10: ERP Enterprise Features](#phase-10-erp-enterprise-features) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100% |\n| 10 | [Phase 10: Production Readiness & Deployment](#phase-10-production-readiness--deployment) | \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 0% |\n| 11 | [Phase 11: Package Split & MX Pattern](#phase-11-package-split--mx-pattern) | \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 100% |\n| 12 | [Phase 12: LLM-Ready Documentation & Automation](#phase-12-llm-ready-documentation--automation) | \u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591\u2591 39% |\n| 13 | [Phase 13: UI/UX Polish & Design System Enhancement](#phase-13-uiux-polish--design-system-enhancement) | \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 0% |\n| 14 | [Phase 14: Studio Enhancements](#phase-14-studio-enhancements) | \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 0% |\n\n---\n\n## Phase 01: Core Kernel & Interfaces\n\n**Phase**: 01\n**Objective**: Set up the project foundation with clean hexagonal architecture. Define all protocol interfaces (ports) without any implementation.\n**Status**: 100% Complete\n\n### 1. Project Initialization\n\n**Progress**: 17/17 (100%)\n\n**Completed:**\n\n- \u2705 Create project structure using `uv`:\n- \u2705 Setup `pyproject.toml` with dependencies\n- \u2705 Add `litestar[standard]>=2.0.0`\n- \u2705 Add `sqlalchemy[asyncio]>=2.0.0` (ensure agnostic, test with sqlite)\n- \u2705 Add `pydantic>=2.0.0`\n- \u2705 Add `pydantic-settings>=2.0.0`\n- \u2705 Add `dependency-injector>=4.41.0` (powerful DI container)\n- \u2705 Add `asyncpg>=0.29.0` (PostgreSQL async driver)\n- \u2705 Add `redis>=5.0.0`\n- \u2705 Remove `arq>=0.26.0`\n- \u2705 Add `taskiq>=0.11.0`, `taskiq-nats>=0.4.0`, `nats-py>=2.0.0`\n- \u2705 Add dev dependencies: `pytest`, `pytest-asyncio`, `mypy`, `ruff`\n- \u2705 Configure development tools\n- \u2705 Setup `mypy` with strict mode in `pyproject.toml`\n- \u2705 Configure `ruff` for linting and formatting\n- \u2705 Add `.gitignore` for Python projects\n- \u2705 Create directory structure\n\n### 2. Define Port Interfaces > 2.1 Repository Protocol\n\n**Progress**: 16/16 (100%)\n\n**Completed:**\n\n- \u2705 Create `tests/core/interfaces/test_repository.py`\n- \u2705 Define test for `RepositoryProtocol` interface compliance\n- \u2705 Create `src/framework_m/core/interfaces/repository.py`\n- \u2705 Define supporting models:\n- \u2705 `FilterSpec` - Typed filter specification (field, operator, value)\n- \u2705 `OrderSpec` - Sorting specification (field, direction)\n- \u2705 `PaginatedResult[T]` - Result with `items`, `total`, `limit`, `offset`, `has_more`\n- \u2705 Define `RepositoryProtocol[T]` (Generic):\n- \u2705 `async def get(self, id: UUID) -> T | None`\n- \u2705 `async def save(self, entity: T, version: int | None = None) -> T` (OCC support)\n- \u2705 `async def delete(self, id: UUID) -> None`\n- \u2705 `async def exists(self, id: UUID) -> bool`\n- \u2705 `async def count(self, filters: list[FilterSpec] | None = None) -> int`\n- \u2705 `async def list(self, filters: list[FilterSpec] | None, order_by: list[OrderSpec] | None, limit: int, offset: int) -> PaginatedResult[T]`\n- \u2705 `async def bulk_save(self, entities: list[T]) -> list[T]`\n- \u2705 Add type hints with `Generic[T]` and `Protocol`\n\n### 2. Define Port Interfaces > 2.2 Event Bus Protocol\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/event_bus.py`\n- \u2705 Define `Event` base model with `id`, `timestamp`, `source`, `type`, `data`\n- \u2705 Define `EventBusProtocol` with methods:\n- \u2705 `async def connect(self) -> None` (for NATS/Kafka)\n- \u2705 `async def disconnect(self) -> None`\n- \u2705 `def is_connected(self) -> bool`\n- \u2705 `async def publish(self, topic: str, event: Event) -> None`\n- \u2705 `async def subscribe(self, topic: str, handler: Callable[[Event], Awaitable[None]]) -> str` (returns subscription_id)\n- \u2705 `async def subscribe_pattern(self, pattern: str, handler: Callable) -> str` (e.g., `doc.*`)\n- \u2705 `async def unsubscribe(self, subscription_id: str) -> None`\n\n### 2. Define Port Interfaces > 2.3 Auth Context Protocol\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/auth_context.py`\n- \u2705 Define `UserContext` Pydantic model with fields:\n- \u2705 `id: str`\n- \u2705 `email: str`\n- \u2705 `roles: list[str]`\n- \u2705 `tenants: list[str]`\n- \u2705 Define `AuthContextProtocol` with methods:\n- \u2705 `async def get_current_user() -> UserContext`\n\n### 2. Define Port Interfaces > 2.4 Storage Protocol\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/storage.py`\n- \u2705 Define `FileMetadata` model with `path`, `size`, `content_type`, `modified`, `etag`\n- \u2705 Define `StorageProtocol` with methods:\n- \u2705 `async def save_file(self, path: str, content: bytes, content_type: str | None = None) -> str`\n- \u2705 `async def get_file(self, path: str) -> bytes`\n- \u2705 `async def delete_file(self, path: str) -> None`\n- \u2705 `async def list_files(self, prefix: str) -> list[str]`\n- \u2705 `async def get_metadata(self, path: str) -> FileMetadata | None`\n- \u2705 `async def get_url(self, path: str, expires: int = 3600) -> str` (presigned URL for S3)\n- \u2705 `async def copy(self, src: str, dest: str) -> str`\n- \u2705 `async def move(self, src: str, dest: str) -> str`\n\n### 2. Define Port Interfaces > 2.5 Job Queue Protocol\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/job_queue.py`\n- \u2705 Define `JobStatus` enum: `PENDING`, `RUNNING`, `SUCCESS`, `FAILED`, `CANCELLED`\n- \u2705 Define `JobInfo` model with `id`, `name`, `status`, `enqueued_at`, `started_at`, `result`, `error`\n- \u2705 Define `JobQueueProtocol` with methods:\n- \u2705 `async def enqueue(self, job_name: str, **kwargs) -> str` (returns job_id)\n- \u2705 `async def schedule(self, job_name: str, cron: str, **kwargs) -> str`\n- \u2705 `async def cancel(self, job_id: str) -> bool`\n- \u2705 `async def get_status(self, job_id: str) -> JobInfo | None`\n- \u2705 `async def retry(self, job_id: str) -> str` (returns new job_id)\n\n### 2. Define Port Interfaces > 2.6 Permission Protocol\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/permission.py`\n- \u2705 Define `PermissionProtocol` with methods:\n- \u2705 `async def has_permission(user: UserContext, doctype: str, action: str, doc_id: str | None) -> bool`\n- \u2705 `async def get_permitted_filters(user: UserContext, doctype: str) -> dict`\n- \u2705 Add action types: `\"read\"`, `\"write\"`, `\"create\"`, `\"delete\"`, `\"submit\"`\n\n### 2. Define Port Interfaces > 2.7 Print Protocol\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/print.py`\n- \u2705 Define `PrintProtocol` with methods:\n- \u2705 `async def render(doc: BaseModel, template: str, format: str = \"pdf\") -> bytes`\n\n### 2. Define Port Interfaces > 2.8 Cache Protocol\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/cache.py`\n- \u2705 Define `CacheProtocol`:\n- \u2705 `async def get(self, key: str) -> Any | None`\n- \u2705 `async def set(self, key: str, value: Any, ttl: int | None = None) -> None`\n- \u2705 `async def delete(self, key: str) -> None`\n- \u2705 `async def exists(self, key: str) -> bool`\n- \u2705 `async def get_many(self, keys: list[str]) -> dict[str, Any]`\n- \u2705 `async def set_many(self, items: dict[str, Any], ttl: int | None = None) -> None`\n- \u2705 `async def delete_pattern(self, pattern: str) -> int` (returns count deleted)\n- \u2705 `async def ttl(self, key: str) -> int | None` (remaining TTL)\n\n### 2. Define Port Interfaces > 2.9 Notification Protocol\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/notification.py`\n- \u2705 Define `NotificationProtocol`:\n- \u2705 `async def send_email(to: str, subject: str, body: str)`\n- \u2705 `async def send_sms(to: str, body: str)`\n\n### 2. Define Port Interfaces > 2.10 Search Protocol\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/search.py`\n- \u2705 Define `SearchResult` model with `items`, `total`, `facets`, `highlights`\n- \u2705 Define `SearchProtocol`:\n- \u2705 `async def index(self, doctype: str, doc_id: str, doc: dict) -> None`\n- \u2705 `async def delete_index(self, doctype: str, doc_id: str) -> None`\n- \u2705 `async def search(self, doctype: str, query: str, filters: dict | None = None, limit: int = 20, offset: int = 0) -> SearchResult`\n- \u2705 `async def reindex(self, doctype: str) -> int` (returns count indexed)\n\n### 2. Define Port Interfaces > 2.11 I18n Protocol\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/i18n.py`\n- \u2705 Define `I18nProtocol`:\n- \u2705 `async def translate(text: str, locale: str) -> str`\n- \u2705 `async def get_locale() -> str`\n\n### 3. Define Domain Layer > 3.1 Base DocType\n\n**Progress**: 12/12 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/domain/base_doctype.py`\n- \u2705 Define `BaseDocType` class inheriting from `pydantic.BaseModel`\n- \u2705 Add standard fields:\n- \u2705 `name: Optional[str]` (primary key, auto-generated if None)\n- \u2705 `creation: datetime`\n- \u2705 `modified: datetime`\n- \u2705 `modified_by: Optional[str]`\n- \u2705 `owner: Optional[str]`\n- \u2705 Add `Meta` nested class for metadata:\n- \u2705 `layout: dict = {}` (via `get_layout()`)\n- \u2705 `permissions: dict = {}` (via `get_permissions()`)\n- \u2705 Add class method `get_doctype_name() -> str`\n\n### 3. Define Domain Layer > 3.2 Base Controller\n\n**Progress**: 12/12 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/domain/base_controller.py`\n- \u2705 Define `BaseController[T]` generic class\n- \u2705 Add lifecycle hook methods:\n- \u2705 `async def validate(self, context: Any = None) -> None`\n- \u2705 `async def before_insert(self, context: Any = None) -> None`\n- \u2705 `async def after_insert(self, context: Any = None) -> None`\n- \u2705 `async def before_save(self, context: Any = None) -> None`\n- \u2705 `async def after_save(self, context: Any = None) -> None`\n- \u2705 `async def before_delete(self, context: Any = None) -> None`\n- \u2705 `async def after_delete(self, context: Any = None) -> None`\n- \u2705 `async def on_submit(self, context: Any = None) -> None`\n- \u2705 `async def on_cancel(self, context: Any = None) -> None`\n\n### 3. Define Domain Layer > 3.3 Mixins\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/domain/mixins.py`\n- \u2705 Define `DocStatus` enum:\n- \u2705 `DRAFT = 0`\n- \u2705 `SUBMITTED = 1`\n- \u2705 `CANCELLED = 2`\n- \u2705 Define `SubmittableMixin` class with:\n- \u2705 `docstatus: DocStatus = DocStatus.DRAFT`\n- \u2705 `def is_submitted() -> bool`\n- \u2705 `def is_cancelled() -> bool`\n- \u2705 `def can_edit() -> bool` (returns False if submitted)\n\n### 4. Meta Registry\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/registry.py`\n- \u2705 Implement `MetaRegistry` as singleton\n- \u2705 Add storage dictionaries:\n- \u2705 `_doctypes: Dict[str, Type[BaseDocType]]`\n- \u2705 `_controllers: Dict[str, Type[BaseController]]`\n- \u2705 Implement methods:\n- \u2705 `register_doctype(doctype_class, controller_class=None)`\n- \u2705 `get_doctype(name: str) -> Type[BaseDocType]`\n- \u2705 `get_controller(name: str) -> Type[BaseController] | None`\n- \u2705 `list_doctypes() -> list[str]`\n- \u2705 `discover_doctypes(package_name: str)` (scans for BaseDocType subclasses)\n- \u2705 **Load Order**: Follows `installed_apps` list.\n- \u2705 **Conflict Policy**: Raise `DuplicateDocTypeError` if same name registered twice.\n\n### 5. Port Implementation (Adapters) > 5.1 Dependency Injection\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `tests/core/test_container.py`\n- \u2705 Write test for Container initialization and service resolution\n- \u2705 Create `src/framework_m/core/container.py`\n- \u2705 Implement `Container` class (using `dependency_injector`):\n- \u2705 Define `Container` class:\n\n### 5. Port Implementation (Adapters) > 5.2 Provider Types\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Understand provider types:\n- \u2705 `Singleton` - Single instance shared across app\n- \u2705 `Factory` - New instance each time\n- \u2705 `Resource` - For resources with lifecycle (db connections)\n- \u2705 `Callable` - For functions\n- \u2705 `Dependency` - For protocol injection\n\n### 5. Port Implementation (Adapters) > 5.3 Configuration Provider\n\n**Progress**: 1/1 (100%)\n\n**Completed:**\n\n- \u2705 Setup configuration loading:\n\n### 5. Port Implementation (Adapters) > 5.4 Wiring\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Configure wiring for automatic injection:\n- \u2705 Use `@inject` decorator in functions:\n\n### 5. Port Implementation (Adapters) > 5.5 Entrypoint Scanning for Overrides\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create override mechanism:\n- \u2705 Scan `framework_m.overrides` entrypoints\n- \u2705 Allow apps to override providers\n- \u2705 Example:\n- \u2705 Implement override loader:\n\n### 5. Port Implementation (Adapters) > 5.6 Testing Support\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Use `override` for testing:\n- \u2705 Reset overrides after tests:\n\n### 6. Testing Setup\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `tests/` directory structure\n- \u2705 Setup `conftest.py` with fixtures:\n- \u2705 `pytest_asyncio` configuration\n- \u2705 Mock implementations of all protocols\n- \u2705 Write initial tests:\n- \u2705 Test `MetaRegistry` registration and retrieval\n- \u2705 Test `BaseDocType` field validation\n- \u2705 Test `BaseController` hook method existence\n\n### 7. Documentation\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `README.md` with:\n- \u2705 Project overview\n- \u2705 Installation instructions\n- \u2705 Basic usage example\n- \u2705 Create `docs/` folder with:\n- \u2705 `architecture.md` - Hexagonal architecture explanation\n- \u2705 `ports.md` - List of all protocol interfaces (Repository, EventBus, Cache, Search, etc.)\n\n### Validation Checklist\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 All protocol interfaces are defined with proper type hints\n- \u2705 `mypy --strict` passes with no errors\n- \u2705 No infrastructure dependencies in `core/` (no imports of litestar, sqlalchemy, redis)\n- \u2705 `BaseDocType` and `BaseController` are properly generic\n- \u2705 `MetaRegistry` can register and retrieve DocTypes\n- \u2705 All tests pass with `pytest`\n\n---\n\n## Phase 02: DocType Engine & Database\n\n**Phase**: 02\n**Objective**: Implement the metadata engine that dynamically creates database tables from Pydantic models and provides generic CRUD operations.\n**Status**: 100% Complete\n\n### 1. Schema Mapper (Pydantic \u2192 SQLAlchemy) > 1.1 Field Registry & Type Mapping\n\n**Progress**: 15/15 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/db/field_registry.py`\n- \u2705 Implement `FieldRegistry` class (Singleton):\n- \u2705 Start with standard types: str, int, float, bool, datetime, date, decimal, uuid, json\n- \u2705 Allow registration of custom types: `register_type(pydantic_type, sqlalchemy_type)`\n- \u2705 Define type mapping using Registry:\n- \u2705 `str` \u2192 `String`\n- \u2705 `int` \u2192 `Integer`\n- \u2705 `float` \u2192 `Float`\n- \u2705 `bool` \u2192 `Boolean`\n- \u2705 `datetime` \u2192 `DateTime`\n- \u2705 `date` \u2192 `Date`\n- \u2705 `Decimal` \u2192 `Numeric`\n- \u2705 `UUID` \u2192 `UUID`\n- \u2705 `list[str]` \u2192 `JSON` (portable, works on all databases)\n- \u2705 `dict` \u2192 `JSON`\n\n### 1. Schema Mapper (Pydantic \u2192 SQLAlchemy) > 1.2 Schema Mapper Implementation\n\n**Progress**: 19/19 (100%)\n\n**Completed:**\n\n- \u2705 Create `tests/adapters/db/test_schema_mapper.py`\n- \u2705 Test mapping simple fields (str, int)\n- \u2705 Test mapping relationships\n- \u2705 Create `SchemaMapper` class\n- \u2705 Use `FieldRegistry` to look up SQLAlchemy types\n- \u2705 Implement `create_table(model: Type[BaseDocType]) -> Table`:\n- \u2705 Extract table name from model class name (lowercase)\n- \u2705 Iterate over `model.model_fields`\n- \u2705 Map each field to SQLAlchemy column\n- \u2705 Handle `Optional` types (nullable=True)\n- \u2705 Set `id` field as primary key (UUID, auto-generated)\n- \u2705 Set `name` field as unique index (for human-readable lookup)\n- \u2705 **OCC**: Add `_version` (Integer, default=0) if `Meta.concurrency=\"optimistic\"`\n- \u2705 Return SQLAlchemy `Table` object\n- \u2705 Handle special field types:\n- \u2705 Relationships (ForeignKey) - detect by field name pattern `*_id`\n- \u2705 Enums - map to SQLAlchemy String type (database agnostic)\n- \u2705 Nested Pydantic models (`list[BaseModel]`) - store as JSON\n- \u2705 Child DocTypes (`list[DocType]`) - map to Relational Table with Foreign Key\n\n### 1. Schema Mapper (Pydantic \u2192 SQLAlchemy) > 1.3 Table Registry\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `TableRegistry` class to store created tables\n- \u2705 Add methods:\n- \u2705 `register_table(doctype_name: str, table: Table)`\n- \u2705 `get_table(doctype_name: str) -> Table`\n- \u2705 `table_exists(doctype_name: str) -> bool`\n\n### 2. Database Connection Setup > 2.1 Connection Factory & Virtual DocTypes\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/db/connection.py`\n- \u2705 Implement `ConnectionFactory` (supports Multiple Bindings):\n- \u2705 Load `db_binds` from config (e.g., `legacy`, `timescale`)\n- \u2705 Create generic async engine (supports Postgres, SQLite for testing)\n- \u2705 Maintain map of `engine_name -> AsyncEngine`\n\n### 2. Database Connection Setup > 2.2 Session Factory (SQL Support)\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 Add session factory:\n- \u2705 `async def get_session(bind: str = \"default\") -> AsyncSession`\n- \u2705 Support `VirtualDocType` (SQL Bind) by accepting `bind_key` from DocType Meta\n\n### 2. Database Connection Setup > 2.3 Non-SQL Virtual DocTypes (Custom Repositories)\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create concept of `RepositoryOverride`:\n- \u2705 Allow DocType to define `repository_class` in Meta\n- \u2705 `RepositoryFactory` instantiates this instead of `GenericRepository`\n- \u2705 Create `tests/adapters/db/test_repository_factory.py`:\n- \u2705 Implement a Mock `FileRepository` that reads from JSON file\n- \u2705 Register it for a `VirtualDoc`\n- \u2705 Verify `VirtualDoc.get()` calls `FileRepository.get()`\n\n### 3. Generic Repository Implementation\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `tests/adapters/db/test_generic_repository.py`\n- \u2705 Define tests for CRUD operations (mock `AsyncSession`)\n- \u2705 Create `src/framework_m/adapters/db/generic_repository.py`\n- \u2705 Implement `GenericRepository[T]` class\n- \u2705 Constructor dependencies (session is NOT stored here):\n- \u2705 `model: Type[T]`\n- \u2705 `table: Table`\n- \u2705 `controller_class: Type[BaseController] | None`\n- \u2705 `event_bus: EventBusProtocol | None` _(InMemoryEventBus for dev)_\n\n### 3. Generic Repository Implementation > 3.1 CRUD Operations\n\n**Progress**: 49/49 (100%)\n\n**Completed:**\n\n- \u2705 Implement `async def get(session: AsyncSession, id: UUID) -> Optional[T]`:\n- \u2705 Build SELECT query\n- \u2705 Filter `deleted_at IS NULL` (unless specific flag overrides)\n- \u2705 Execute with provided session (not stored session)\n- \u2705 Convert row to Pydantic model\n- \u2705 Return None if not found\n- \u2705 Implement `async def get_by_name(session: AsyncSession, name: str) -> Optional[T]`:\n- \u2705 Helper for looking up by human-readable name\n- \u2705 Implement `async def save(session: AsyncSession, entity: T, version: int | None = None) -> T`:\n- \u2705 Check if entity exists (by `id`)\n- \u2705 If new:\n- \u2705 Generate `id` (UUIDv7)\n- \u2705 **Note**: `created_by` / `owner` must be set by Controller/Service before calling save\n- \u2705 Call controller `validate()`\n- \u2705 Call controller `before_create()`\n- \u2705 Call controller `before_save()`\n- \u2705 Execute INSERT with provided session\n- \u2705 Call controller `after_save()`\n- \u2705 Call controller `after_create()`\n- \u2705 **Emit Event**: `event_bus.publish(f\"{doctype}.create\", payload)`\n- \u2705 If existing:\n- \u2705 **Note**: `modified_by` must be set by Controller/Service before calling save\n- \u2705 Call controller `validate()`\n- \u2705 Call controller `before_save()`\n- \u2705 **OCC**: If `optimistic`:\n- \u2705 `UPDATE table SET ..., _version=_version+1 WHERE id=:id AND _version=:old_ver`\n- \u2705 If rowcount == 0, raise `VersionConflictError`\n- \u2705 Else: Execute standard UPDATE\n- \u2705 Call controller `after_save()`\n- \u2705 **Emit Event**: `event_bus.publish(f\"{doctype}.update\", payload)`\n- \u2705 Return saved entity (caller calls `uow.commit()` when ready)\n- \u2705 Implement `async def delete(session: AsyncSession, id: UUID, hard: bool = False) -> None`:\n- \u2705 Load entity\n- \u2705 Call controller `before_delete()`\n- \u2705 If `hard`:\n- \u2705 Execute DELETE\n- \u2705 Else (Soft Delete):\n- \u2705 Update `deleted_at = now()`\n- \u2705 Call controller `after_delete()`\n- \u2705 **Emit Event**: `event_bus.publish(f\"{doctype}.delete\", payload)`\n- \u2705 Return (caller calls `uow.commit()` when ready)\n- \u2705 Implement `async def list(session: AsyncSession, filters, limit, offset, order_by) -> Sequence[T]`:\n- \u2705 Build SELECT query with filters\n- \u2705 Default filter: `deleted_at IS NULL`\n- \u2705 Apply pagination (limit, offset)\n- \u2705 Apply sorting (order_by)\n- \u2705 Execute query\n- \u2705 Convert rows to Pydantic models\n- \u2705 Return list\n\n### 3. Generic Repository Implementation > 3.2 Lifecycle Hook Integration\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create helper method `_call_hook(hook_name: str)`:\n- \u2705 Check if controller exists\n- \u2705 Check if hook method exists on controller\n- \u2705 Call hook method if present\n- \u2705 Handle exceptions and rollback on error\n\n### 4. Migration System > 4.1 Alembic Integration\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Initialize Alembic in project:\n- \u2705 Configure `alembic.ini`:\n- \u2705 Set database URL from environment\n- \u2705 Configure migration file location\n- \u2705 Create `alembic/env.py`:\n- \u2705 Import `MetaRegistry`\n- \u2705 Import all registered DocTypes\n- \u2705 Set `target_metadata` from SchemaMapper tables\n\n### 4. Migration System > 4.2 Auto-Migration Detection\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/db/migration.py`\n- \u2705 Implement `detect_schema_changes()`:\n- \u2705 Compare registered DocTypes with database schema\n- \u2705 Detect new tables\n- \u2705 Detect new columns\n- \u2705 Detect type changes\n- \u2705 Return list of changes\n- \u2705 Implement `auto_migrate()`:\n- \u2705 Call `detect_schema_changes()`\n- \u2705 Generate Alembic migration if changes detected\n- \u2705 Apply migration automatically (dev mode only)\n\n### 4. Migration System > 4.3 CLI Commands\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Add migration commands to CLI:\n- \u2705 `m migrate` - run pending migrations\n- \u2705 `m migrate:create ` - create new migration\n- \u2705 `m migrate:rollback` - rollback last migration\n- \u2705 `m migrate:status` - show migration status\n\n### 5. Repository Factory\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/db/repository_factory.py`\n- \u2705 Implement `RepositoryFactory` class:\n- \u2705 `create_generic_repository(doctype_name: str) -> GenericRepository`\n- \u2705 Look up DocType from MetaRegistry\n- \u2705 Look up Table from TableRegistry\n- \u2705 Look up Controller from MetaRegistry\n- \u2705 Create and return GenericRepository instance\n- \u2705 Support `event_bus` parameter for domain events\n- \u2705 Support custom repository overrides for Virtual DocTypes\n\n### 6. Engine, Session Factory & Unit of Work > 6.1 Engine & Session Factory Setup\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/db/connection.py` _(ConnectionFactory)_\n- \u2705 Implement `create_engine(url: str) -> AsyncEngine`\n- \u2705 Pool configuration (pool_size, max_overflow, timeout, recycle, pre_ping)\n- \u2705 Environment variable expansion (`${VAR}` syntax)\n- \u2705 Implement `SessionFactory` (returns `AsyncSession` context managers)\n- \u2705 Support multiple binds (for Virtual DocTypes with SQL binds)\n- \u2705 Configuration from ConnectionFactory\n- \u2705 Auto commit/rollback in context manager\n\n### 6. Engine, Session Factory & Unit of Work > 6.2 Unit of Work (`UnitOfWork`)\n\n**Progress**: 14/14 (100%)\n\n**Completed:**\n\n- \u2705 Create `tests/core/test_unit_of_work.py`\n- \u2705 Test: UoW provides session\n- \u2705 Test: `commit()` persists changes\n- \u2705 Test: Exception causes rollback (no explicit rollback call needed)\n- \u2705 Test: Session is closed after `__aexit__`\n- \u2705 Create `src/framework_m/core/unit_of_work.py`\n- \u2705 Implement `UnitOfWork` context manager:\n- \u2705 `__init__(session_factory: Callable[[], AsyncSession])`\n- \u2705 `session` property with safety check\n- \u2705 `async __aenter__()` creates session\n- \u2705 `async commit()` calls session.commit()\n- \u2705 `async rollback()` for explicit rollback\n- \u2705 `async __aexit__()` with auto-rollback on exception\n- \u2705 Register `UnitOfWorkFactory` in DI container\n\n### 6. Engine, Session Factory & Unit of Work > 6.3 Multi-Source Coordination (Outbox Pattern)\n\n**Progress**: 17/17 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/domain/outbox.py`\n- \u2705 Define `OutboxEntry` model:\n- \u2705 `id: UUID`\n- \u2705 `target: str` (e.g., \"mongodb.audit_log\", \"api.payment_gateway\")\n- \u2705 `payload: dict`\n- \u2705 `status: str` (pending, processed, failed)\n- \u2705 `created_at: datetime`\n- \u2705 `processed_at: datetime | None`\n- \u2705 `error_message: str | None`\n- \u2705 `retry_count: int`\n- \u2705 Create `OutboxRepository` (SQL-backed) in `adapters/db/outbox_repository.py`\n- \u2705 `add(session, entry)` - add entry in same transaction\n- \u2705 `get_pending(session, limit)` - get pending entries\n- \u2705 `mark_processed(session, id)` - mark as processed\n- \u2705 `mark_failed(session, id, error)` - mark as failed\n- \u2705 Document: Services write to Outbox in the _same_ SQL transaction\n- \u2705 (Phase 04) Background worker processes Outbox entries\n\n### 6. Engine, Session Factory & Unit of Work > 6.4 Startup Sequence (Schema Sync)\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/db/__init__.py`\n- \u2705 Implement startup sequence (called once at app boot, NOT per-request):\n- \u2705 Initialize database engine (NOT session)\n- \u2705 Discover all DocTypes via MetaRegistry\n- \u2705 Create/sync tables via SchemaMapper\n- \u2705 Register tables in TableRegistry\n- \u2705 Run auto-migration (if enabled)\n\n### 7. Testing > 7.1 Unit Tests\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Test `SchemaMapper`:\n- \u2705 Test type mapping for all supported types (`test_schema_mapper.py`)\n- \u2705 Test primary key creation (`test_table_has_id_column_as_primary_key`)\n- \u2705 Test nullable fields (`TestSchemaMapperNullableFields`)\n- \u2705 Test enum mapping (`TestSchemaMapperEnums`)\n- \u2705 Test `GenericRepository`:\n- \u2705 Test CRUD operations with mock data (`test_generic_repository.py`)\n- \u2705 Test lifecycle hook calls (`TestControllerHooks`)\n- \u2705 Test transaction rollback on error (`TestTransactionRollback`)\n\n### 7. Testing > 7.2 Integration Tests\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Setup testcontainers for PostgreSQL (skips if Docker unavailable)\n- \u2705 Create test DocType (`IntegrationTestDoc` / `PostgresTestDoc`)\n- \u2705 Test full flow (SQLite & Postgres - Postgres skipped without Docker):\n- \u2705 Register DocType (`test_register_doctype`)\n- \u2705 Create table (`test_table_created`)\n- \u2705 Insert document (`test_insert_document`)\n- \u2705 Query document (`test_query_document`)\n- \u2705 Update document (`test_update_document`)\n- \u2705 Delete document (`test_delete_document`)\n- \u2705 Test migration:\n- \u2705 Add field to DocType\n- \u2705 Run auto-migration\n- \u2705 Verify column added to table\n\n### 8. Error Handling\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create custom exceptions (`core/exceptions.py`):\n- \u2705 `DocTypeNotFoundError`\n- \u2705 `ValidationError`\n- \u2705 `PermissionDeniedError`\n- \u2705 `DuplicateNameError`\n- \u2705 `RepositoryError`, `EntityNotFoundError`, `DatabaseError`, `IntegrityError`\n- \u2705 Add error handling in repository (`generic_repository.py`):\n- \u2705 Catch SQLAlchemy errors (`IntegrityError`, `OperationalError`, `SQLAlchemyError`)\n- \u2705 Convert to domain exceptions (`DuplicateNameError`, `DatabaseError`, etc.)\n- \u2705 Log errors with context (using `logger.error` with `extra` dict)\n\n### 9. Performance Optimizations\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Add query result caching (phase-04):\n- \u2705 Cache `get()` results by ID\n- \u2705 Invalidate cache on save/delete\n- \u2705 Use Redis for distributed cache\n- \u2705 Add bulk operations (`generic_repository.py`):\n- \u2705 `async def bulk_save(entities)` - separates new/existing\n- \u2705 `async def _bulk_insert(entities)` - true SQLAlchemy bulk insert\n- \u2705 Uses `insert().values([...])` for performance\n- \u2705 Add query optimization (add indexes after the API layer exists):\n- \u2705 Add indexes for common queries (schema_mapper: owner, creation, Meta.indexes)\n- \u2705 Add parent index for child tables (efficient joins)\n- \u2705 Use `select_in_loading` for relationships (`load_children_for_parents` method)\n- \u2705 Add query logging in dev mode (`logger.debug` for GET/LIST)\n\n### Validation Checklist\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Can create tables from Pydantic models dynamically\n- \u2705 CRUD operations work with lifecycle hooks\n- \u2705 Migrations are generated and applied correctly\n- \u2705 All integration tests pass with real PostgreSQL _(skips if Docker unavailable)_\n- \u2705 No direct SQLAlchemy imports in domain layer\n- \u2705 Repository implements `RepositoryProtocol` correctly\n\n---\n\n## Phase 03: API Layer & Authorization\n\n**Phase**: 03\n**Objective**: Expose DocTypes via HTTP with auto-generated CRUD endpoints, implement permission system with row-level security, and add RPC support.\n**Status**: 100% Complete\n\n### 1. Litestar Application Setup\n\n**Progress**: 14/14 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/web/app.py`\n- \u2705 Initialize Litestar app:\n- \u2705 Configure CORS for development\n- \u2705 Add exception handlers\n- \u2705 Configure OpenAPI documentation\n- \u2705 Integrate with `dependency-injector` Container (wire modules)\n- \u2705 **Middleware Discovery**:\n- \u2705 Scan `MiddlewareRegistry` for registered App Middlewares.\n- \u2705 Allow Apps to inject ASGI Middleware (e.g. `RateLimit`, `cors`, `Compression`).\n- \u2705 **APM & Tracing**: Support OpenTelemetry, DataDog, Sentry via standard ASGI middleware.\n- \u2705 Support ordering (priority).\n- \u2705 Add application lifecycle:\n- \u2705 `on_startup`: Initialize Container, database, discover DocTypes\n- \u2705 `on_shutdown`: Close database connections, unwire Container\n\n### 2. Authentication Middleware\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/web/middleware.py`\n- \u2705 Implement `AuthMiddleware`:\n- \u2705 Extract `x-user-id` header\n- \u2705 Extract `x-roles` header (comma-separated)\n- \u2705 Extract `x-tenants` header (optional, comma-separated)\n- \u2705 Create `UserContext` object\n- \u2705 Store in `request.state.user`\n- \u2705 Return 401 if headers missing (configurable)\n- \u2705 Add middleware to Litestar app _(available via `create_auth_middleware()`)_\n\n### 3. Permission System > 3.1 Permission Protocol Implementation\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/auth/rbac_permission.py`\n- \u2705 Implement `RbacPermissionAdapter`:\n- \u2705 Implement `evaluate(request: PolicyEvaluateRequest) -> PolicyEvaluateResult`\n- \u2705 Read permissions from `DocType.Meta.permissions`\n- \u2705 Match `request.principal_attributes[\"roles\"]` against allowed roles\n- \u2705 `CustomPermission` DocType created _(integration pending)_\n- \u2705 Return `PolicyEvaluateResult(authorized=True/False, decision_source=DecisionSource.RBAC)`\n\n### 3. Permission System > 3.2 Permission Configuration\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Define permission structure in DocType:\n- \u2705 Implement `has_permission()`:\n- \u2705 Check `requires_auth` \u2014 if False and no user, allow read\n- \u2705 Check if user has required role\n- \u2705 Check database overrides via `PermissionLookupService` (CustomPermission DocType)\n- \u2705 For object-level: check ownership or custom rules\n- \u2705 Return boolean\n\n### 3. Permission System > 3.3 System Context (Elevated Operations)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/system_context.py`\n- \u2705 Implement `SystemContext`:\n- \u2705 Usage in background jobs:\n- \u2705 **Audit**: All system operations logged with `principal=\"system\"`\n\n### 3. Permission System > 3.4 Row-Level Security (RLS)\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Implement `get_permitted_filters()`:\n- \u2705 For standard users: add `WHERE owner = :user_id`\n- \u2705 For admins: no filter (see all)\n- \u2705 For custom rules: via `Meta.rls_field` (completed by Section 3.5 Team-Based Access)\n- \u2705 Return dict of SQLAlchemy filters\n- \u2705 Update `GenericRepository.list()`:\n- \u2705 `list_for_user()` method with RLS filtering\n- \u2705 `apply_rls_filters()` helper available\n- \u2705 Merge with user-provided filters\n- \u2705 Apply to SQL query\n- \u2705 Update `GenericRepository.get()`:\n- \u2705 `get_for_user()` method with permission check\n- \u2705 Raise `PermissionDeniedError` if denied\n\n### 3. Permission System > 3.5 Team-Based Access (Indie)\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Add `Meta.rls_field` option:\n- \u2705 Update `get_permitted_filters()`:\n- \u2705 If `rls_field = \"owner\"` (default): `WHERE owner = :user_id`\n- \u2705 If `rls_field = \"team\"`: `WHERE team IN :user_teams`\n- \u2705 Support custom fields\n\n### 3. Permission System > 3.6 Explicit Sharing (`DocumentShare`)\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create built-in `DocumentShare` DocType:\n- \u2705 Update `get_permitted_filters()` to include shares via `get_rls_filters_with_shares()`:\n- \u2705 Add share management API:\n- \u2705 `POST /api/v1/share` \u2014 Create share\n- \u2705 `DELETE /api/v1/share/{id}` \u2014 Remove share\n- \u2705 `GET /api/v1/{doctype}/{id}/shares` \u2014 List shares for a doc\n\n### 3. Permission System > 3.7 Indie Mode: Permission Conveniences\n\n**Progress**: 14/14 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/decorators.py` (if not exists)\n- \u2705 Implement `@requires_permission(action: PermissionAction)`:\n- \u2705 Internally builds `PolicyEvaluateRequest` from `self.user`, `self.doctype_name`\n- \u2705 Calls `permission.evaluate(request)`\n- \u2705 Raises `PermissionDeniedError` if not authorized\n- \u2705 Works on Controller methods\n- \u2705 Add to `BaseController`:\n- \u2705 Add to `BaseController`:\n- \u2705 Verify: All auto-generated CRUD routes (Section 4) automatically call `permission.evaluate()`:\n- \u2705 `POST /api/v1/{doctype}` \u2192 checks `CREATE` permission _(verified via test)_\n- \u2705 `GET /api/v1/{doctype}/{id}` \u2192 checks `READ` permission _(verified via test)_\n- \u2705 `PUT /api/v1/{doctype}/{id}` \u2192 checks `WRITE` permission _(verified via test)_\n- \u2705 `DELETE /api/v1/{doctype}/{id}` \u2192 checks `DELETE` permission _(verified via test)_\n- \u2705 **No manual code required** for indie devs using auto-CRUD _(verified via test)_\n\n### 4. Auto-CRUD Router > 4.1 Meta Router Implementation\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/web/meta_router.py`\n- \u2705 Implement `create_crud_routes(doctype_class: type[BaseDocType])`:\n- \u2705 **Check Opt-in**: Verify `DocType.Meta.api_resource is True`. Skip if False.\n- \u2705 Generate 5 routes per DocType\n- \u2705 Inject repository and permission service\n- \u2705 Parse filters from JSON string via `_parse_filters()`\n\n### 4. Auto-CRUD Router > 4.2 List Endpoint\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/{doctype}`:\n- \u2705 Accept query parameters: `limit`, `offset`, `filters`, `order_by`\n- \u2705 Parse filters from JSON string\n- \u2705 Call repository `list_entities()` with RLS filters via `apply_rls_filters()`\n- \u2705 Return paginated response with metadata:\n\n### 4. Auto-CRUD Router > 4.3 Create Endpoint\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `POST /api/v1/{doctype}`:\n- \u2705 Validate request body against Pydantic model\n- \u2705 Check `CREATE` permission via `_check_permission()`\n- \u2705 Set `owner` to current user (`data[\"owner\"] = user_id`)\n- \u2705 Call repository `save()`\n- \u2705 Return 201 with created document\n\n### 4. Auto-CRUD Router > 4.4 Read Endpoint\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/{doctype}/{id}`:\n- \u2705 Call repository `get(id)`\n- \u2705 Check `READ` permission\n- \u2705 Return 404 if not found (via `NotFoundException`)\n- \u2705 Return 403 if permission denied\n- \u2705 Return document\n\n### 4. Auto-CRUD Router > 4.5 Update Endpoint\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `PUT /api/v1/{doctype}/{id}`:\n- \u2705 Load existing document via `repo.get()`\n- \u2705 Check `WRITE` permission\n- \u2705 Check if submitted (deny if immutable) via `_check_submitted()`\n- \u2705 Merge changes via `entity.model_copy(update=data)`\n- \u2705 Call repository `save()`\n- \u2705 Return updated document\n\n### 4. Auto-CRUD Router > 4.6 Delete Endpoint\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `DELETE /api/v1/{doctype}/{id}`:\n- \u2705 Load document\n- \u2705 Check `DELETE` permission\n- \u2705 Check if submitted (deny if immutable) via `_check_submitted()`\n- \u2705 Call repository `delete()`\n- \u2705 Return 204 No Content\n\n### 4. Auto-CRUD Router > 4.7 Router Registration\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement `create_meta_router()`:\n- \u2705 Get all DocTypes from MetaRegistry\n- \u2705 For each DocType with `api_resource=True`, call `create_crud_routes()`\n- \u2705 Register all routes with Litestar\n- \u2705 Return Router instance\n\n### 5. RPC System > 5.1 Whitelisted Controller Methods\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Create `@whitelist` decorator:\n- \u2705 Mark controller methods as publicly callable\n- \u2705 Store metadata on method (`WHITELIST_ATTR`)\n- \u2705 `is_whitelisted()` helper function\n- \u2705 `get_whitelist_options()` helper function\n- \u2705 Create `POST /api/v1/rpc/{doctype}/{method}`:\n- \u2705 Load DocType controller\n- \u2705 Check if method has `@whitelist` decorator\n- \u2705 Validate method exists\n- \u2705 Parse request body as method arguments\n- \u2705 Instantiate controller with document (if doc_id provided)\n- \u2705 Call method\n- \u2705 Return result\n\n### 5. RPC System > 5.2 Arbitrary RPC Functions\n\n**Progress**: 16/16 (100%)\n\n**Completed:**\n\n- \u2705 Create `@rpc` decorator for standalone functions:\n- \u2705 Register function in RPC registry (`RpcRegistry` singleton)\n- \u2705 Store function path (e.g., `my_app.api.send_email`)\n- \u2705 `is_rpc_function()` helper\n- \u2705 `get_rpc_options()` helper\n- \u2705 Create `POST /api/v1/rpc/fn/{dotted.path}`:\n- \u2705 Parse dotted path (e.g., `my_app.api.send_email`)\n- \u2705 Look up function in registry\n- \u2705 Validate function has `@rpc` decorator\n- \u2705 Parse request body as function arguments\n- \u2705 Call function\n- \u2705 Return result\n- \u2705 Add permission check for RPC:\n- \u2705 Support `@rpc(permission=\"custom_permission\")`\n- \u2705 Check permission before calling function\n- \u2705 Support `@rpc(allow_guest=True)` for public endpoints\n\n### 6. Metadata API\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/meta/{doctype}`:\n- \u2705 Get DocType class from registry\n- \u2705 Generate JSON Schema from Pydantic model:\n- \u2705 Extract layout from `Meta.layout`\n- \u2705 Extract permissions from `Meta.permissions`\n- \u2705 Return:\n- \u2705 Add field metadata:\n- \u2705 Field labels (from `Field(title=...)`) - included in JSON Schema\n- \u2705 Help text (from `Field(description=...)`) - included in JSON Schema\n- \u2705 Validation rules (from Pydantic validators) - included in JSON Schema\n- \u2705 Field types and options - included in JSON Schema\n\n### 7. OpenAPI Documentation\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Configure Litestar OpenAPI:\n- \u2705 Set title, version, description\n- \u2705 Add authentication scheme (header-based: `x-user-id`, `x-roles`)\n- \u2705 Enable Swagger UI at `/schema/swagger`\n- \u2705 Enable ReDoc at `/schema/redoc`\n- \u2705 Enhance auto-generated docs:\n- \u2705 Add descriptions to CRUD endpoints (via docstrings)\n- \u2705 Add examples for request/response (via Pydantic models)\n- \u2705 Document RPC endpoints (auto-generated from route handlers)\n- \u2705 Add permission requirements to docs (via security schemes)\n\n### 8. Error Handling\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create exception handlers:\n- \u2705 `ValidationError` \u2192 400 Bad Request (`validation_error_handler`)\n- \u2705 `PermissionDeniedError` \u2192 403 Forbidden (`permission_denied_handler`)\n- \u2705 `DocTypeNotFoundError` \u2192 404 Not Found (`not_found_handler`)\n- \u2705 `DuplicateNameError` \u2192 409 Conflict (`duplicate_name_handler`)\n- \u2705 Generic exceptions \u2192 500 Internal Server Error (`framework_error_handler`)\n- \u2705 Return consistent error format:\n\n### 9. Request/Response DTOs\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Use Litestar DTOs for validation:\n- \u2705 Auto-generate from Pydantic models (via Litestar)\n- \u2705 Add DTO for list response (pagination): `PaginatedResponse[T]`\n- \u2705 Add DTO for error responses: `ErrorResponse`\n- \u2705 Implement response filtering:\n- \u2705 Exclude sensitive fields based on permissions (via field selection)\n- \u2705 Support field selection via query param: `?fields=name,title`\n\n### 10. Testing > 10.1 Unit Tests\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Test permission system:\n- \u2705 Test role-based access (`test_rbac_permission.py`)\n- \u2705 Test RLS filter generation (`test_rls.py`)\n- \u2705 Test object-level permissions (`test_object_level_permissions.py`)\n- \u2705 Test RPC system:\n- \u2705 Test `@whitelist` decorator (`test_whitelist_decorator.py`)\n- \u2705 Test `@rpc` decorator (`test_rpc_decorator.py`)\n- \u2705 Test dotted path resolution (`test_rpc_routes.py`)\n\n### 10. Testing > 10.2 Integration Tests\n\n**Progress**: 15/15 (100%)\n\n**Completed:**\n\n- \u2705 Test CRUD endpoints (`test_api_endpoints.py`):\n- \u2705 Test create with valid data\n- \u2705 Test create with invalid data (validation)\n- \u2705 Test create without permission (403)\n- \u2705 Test list with RLS (only see own docs)\n- \u2705 Test update immutable doc (covered by existing `test_meta_router.py`)\n- \u2705 Test delete with permission (covered by existing permission tests)\n- \u2705 Test RPC endpoints (`test_api_endpoints.py`, `test_rpc_routes.py`):\n- \u2705 Test whitelisted controller method\n- \u2705 Test arbitrary RPC function\n- \u2705 Test RPC with permissions\n- \u2705 Test with real HTTP client (`test_real_http_client.py`):\n- \u2705 Uses `httpx.AsyncClient` against real uvicorn server\n- \u2705 Tests: list endpoint, create endpoint, health check\n- \u2705 Manual testing scripts available in `examples/` folder\n\n### Validation Checklist\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 All CRUD endpoints work with permissions (verified via `test_meta_router.py`, `test_api_endpoints.py`)\n- \u2705 RLS filters are applied correctly (verified via `test_rls.py`, `test_object_level_permissions.py`)\n- \u2705 RPC system works for controller methods and functions (verified via `test_rpc_routes.py`)\n- \u2705 OpenAPI docs are generated correctly (SwaggerUI + ReDoc enabled)\n- \u2705 Error handling returns proper status codes (verified via `test_app.py`)\n- \u2705 Integration tests pass with real HTTP requests (verified via `test_real_http_client.py`)\n\n### Edge Cases to Handle > Child Table Permissions\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Define: Child tables inherit parent's RLS by default (`Meta.is_child_table = True`)\n- \u2705 Child rows are not independently permission-checked (`api_resource = False`)\n- \u2705 Loading parent loads all its children (no separate RLS query)\n- \u2705 Document: If independent child access needed, make it a separate DocType (tests in `test_child_table_permissions.py`)\n\n### Edge Cases to Handle > Link Field Data Leakage\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 When serializing a doc with Link fields, do NOT auto-include linked doc data (Link fields store UUID, not embedded objects)\n- \u2705 If linked doc data is needed, make a separate API call (permission checked) - documented pattern\n- \u2705 Optional: `?expand=customer` param design documented (checks permission before expanding)\n\n### Edge Cases to Handle > Bulk Operations & RLS\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 `GenericRepository.delete_many_for_user(filters)` applies RLS\n- \u2705 `GenericRepository.update_many_for_user(filters, data)` applies RLS\n- \u2705 User can only bulk-modify docs they have access to (tests in `test_bulk_operations_rls.py`)\n\n---\n\n## Phase 04: Background Jobs & Events\n\n**Phase**: 04\n**Objective**: Implement asynchronous job processing, event-driven architecture, and webhook system with **pluggable backends** (Redis, NATS, In-Memory).\n**Status**: 98% Complete\n\n### 0. Backend Selection (Cross-Platform) > 0.2 Dev Mode (No External Deps)\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 Implement `InMemoryJobQueue` for local dev (asyncio.Queue).\n- \u2705 Implement `InMemoryEventBus` for local dev.\n- \u2705 Auto-detect: If `NATS_URL` not set, use in-memory.\n\n### 1. Taskiq + NATS Integration (Primary) > 1.1 Taskiq Setup\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Add dependencies: `taskiq`, `taskiq-nats`.\n- \u2705 Create `src/framework_m/adapters/jobs/taskiq_adapter.py`\n- \u2705 Configure Taskiq with NATS JetStream:\n- \u2705 Set NATS connection URL from environment (`NATS_URL`)\n- \u2705 Configure job timeout (default: 300s)\n- \u2705 Configure max retries (default: 3)\n- \u2705 Use `PullBasedJetStreamBroker` for reliable delivery\n- \u2705 Create broker instance:\n\n### 1. Taskiq + NATS Integration (Primary) > 1.2 Job Queue Protocol Implementation\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Implement `TaskiqJobQueueAdapter`:\n- \u2705 `async def enqueue(job_name: str, **kwargs) -> str`:\n- \u2705 Get registered task\n- \u2705 Call `.kiq()` to enqueue\n- \u2705 Return job ID\n- \u2705 `async def schedule(job_name: str, cron: str, **kwargs)`:\n- \u2705 Use `TaskiqScheduler` with cron triggers\n\n### 1. Taskiq + NATS Integration (Primary) > 1.3 Job Registration\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create job decorator `@job`:\n- \u2705 Implement job registry:\n- \u2705 Store all `@broker.task` decorated functions\n- \u2705 Auto-discover on startup\n\n### 2. Worker Process > 2.1 Worker Implementation\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/cli/worker.py`\n- \u2705 Implement worker command:\n- \u2705 Worker startup:\n- \u2705 Initialize database connection\n- \u2705 Discover and register all jobs\n- \u2705 Start Taskiq worker\n- \u2705 Listen for jobs from NATS JetStream\n\n### 2. Worker Process > 2.2 Job Context\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create job context for dependency injection:\n- \u2705 Provide database session\n- \u2705 Provide repository factory\n- \u2705 Provide event bus\n- \u2705 Pass to job functions\n- \u2705 Add job metadata:\n- \u2705 Job ID\n- \u2705 Enqueued time\n- \u2705 Started time\n- \u2705 User context (Must re-hydrate from job arguments if applicable)\n\n### 3. Scheduler > 3.1 Cron Jobs\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Support Taskiq's native cron syntax:\n- \u2705 Create `ScheduledJob` DocType:\n- \u2705 `name: str` - Job identifier\n- \u2705 `function: str` - Dotted path to function\n- \u2705 `cron_expression: str` - Cron syntax\n- \u2705 `enabled: bool` - Active/inactive\n- \u2705 `last_run: datetime | None`\n- \u2705 `next_run: datetime | None`\n\n### 3. Scheduler > 3.2 Dynamic Scheduler\n\n**Progress**: 3/4 (75%)\n\n**Completed:**\n\n- \u2705 Implement dynamic job scheduling:\n- \u2705 Load `ScheduledJob` DocTypes from database\n- \u2705 Register with Taskiq at startup\n\n**Pending:**\n\n- \u23f3 Support adding/removing jobs without restart (via signal) **NOT NEEDED**: Worker restart is acceptable for schedule changes; k8s rolling updates handle this gracefully. If needed in future, can use NATS pub/sub to signal workers in Phase 06+ (Ops/Deployment).\n\n### 4. Event Bus > 4.1 NATS Event Bus Implementation\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/events/nats_event_bus.py`\n- \u2705 Implement `NatsEventBusAdapter`:\n- \u2705 `async def publish(topic: str, event: BaseModel)`:\n- \u2705 Serialize event to JSON\n- \u2705 Publish to NATS JetStream subject\n- \u2705 `async def subscribe(topic: str, handler: Callable)`:\n- \u2705 Subscribe to NATS JetStream subject\n- \u2705 Deserialize event\n- \u2705 Call handler function\n\n### 4. Event Bus > 4.2 Event Types\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create base event class:\n- \u2705 Define standard events:\n- \u2705 `DocCreated`\n- \u2705 `DocUpdated`\n- \u2705 `DocDeleted`\n- \u2705 `DocSubmitted`\n- \u2705 `DocCancelled`\n\n### 4. Event Bus > 4.3 Event Publishing from Lifecycle Hooks\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Update `GenericRepository`:\n- \u2705 Inject event bus\n- \u2705 Publish `DocCreated` after insert\n- \u2705 Publish `DocUpdated` after update\n- \u2705 Publish `DocDeleted` after delete\n- \u2705 Add event publishing to controller hooks:\n\n### 5. Webhook System > 5.1 Webhook DocType\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `Webhook` DocType:\n- \u2705 `name: str`\n- \u2705 `event: str` - Event to listen to (e.g., \"doc.created\")\n- \u2705 `doctype_filter: str | None` - Filter by DocType\n- \u2705 `url: str` - Webhook endpoint URL\n- \u2705 `method: str` - HTTP method (POST, PUT)\n- \u2705 `headers: dict` - Custom headers\n- \u2705 `enabled: bool`\n- \u2705 `secret: str` - For signature verification\n\n### 5. Webhook System > 5.2 Webhook Listener\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/webhooks/listener.py`\n- \u2705 Implement webhook listener:\n- \u2705 Subscribe to all events\n- \u2705 Load active webhooks from database\n- \u2705 Filter events by webhook configuration\n- \u2705 Trigger HTTP call to webhook URL\n\n### 5. Webhook System > 5.3 Webhook Delivery\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Implement webhook delivery:\n- \u2705 Use `httpx` for async HTTP calls\n- \u2705 Add signature header (HMAC-SHA256 with secret)\n- \u2705 Set timeout (default: 30s)\n- \u2705 Handle errors and retries\n- \u2705 Add retry logic:\n- \u2705 Retry on failure (exponential backoff)\n- \u2705 Max retries: 3\n- \u2705 Log failed deliveries\n\n### 5. Webhook System > 5.4 Webhook Logs\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `WebhookLog` DocType:\n- \u2705 `webhook: str` - Reference to Webhook\n- \u2705 `event: str` - Event that triggered\n- \u2705 `status: str` - Success/Failed\n- \u2705 `response_code: int`\n- \u2705 `response_body: str`\n- \u2705 `error: str | None`\n- \u2705 `timestamp: datetime`\n\n### 6. Job Monitoring > 6.1 Job Status Tracking\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Create `JobLog` DocType:\n- \u2705 `job_id: str`\n- \u2705 `job_name: str`\n- \u2705 `status: str` - Queued/Running/Success/Failed\n- \u2705 `enqueued_at: datetime`\n- \u2705 `started_at: datetime | None`\n- \u2705 `completed_at: datetime | None`\n- \u2705 `error: str | None`\n- \u2705 `result: dict | None`\n- \u2705 Update job execution to log status:\n- \u2705 Create log entry on enqueue\n- \u2705 Update on start\n- \u2705 Update on completion/failure\n\n### 6. Job Monitoring > 6.2 Job Management API\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create endpoints:\n- \u2705 `GET /api/v1/jobs` - List jobs\n- \u2705 `GET /api/v1/jobs/{job_id}` - Get job status\n- \u2705 `POST /api/v1/jobs/{job_id}/cancel` - Cancel job\n- \u2705 `POST /api/v1/jobs/{job_id}/retry` - Retry failed job\n\n### 6. Job Monitoring > 6.3 Job Monitor (Admin UI)\n\n**Progress**: 3/6 (50%)\n\n**Completed:**\n\n- \u2705 **Option A: Native Desk UI** (IMPLEMENTED):\n- \u2705 `Job Log` List View (auto-generated via `api_resource=True`).\n- \u2705 Shows Status, Error, Duration (`duration_seconds` property).\n\n**Pending:**\n\n- \u23f3 ~~**Option B: Taskiq Dashboard (External)**~~ - **NOT NEEDED for MVP**: Native JobLog UI is sufficient for debugging. External dashboards can be mounted in Phase 06+ (Ops/Deployment) if production monitoring requires deeper worker introspection.\n- \u23f3 Mount `taskiq-dashboard` (if available) or similar tool.\n- \u23f3 Useful for sysadmins to debug worker health.\n\n### 4. Real-time Events (WebSockets) > 4.1 WebSocket Protocol (The Port)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/socket.py`\n- \u2705 Define `SocketProtocol`:\n- \u2705 `async def broadcast(topic: str, message: dict)`\n- \u2705 `async def send_to_user(user_id: str, message: dict)`\n\n### 4. Real-time Events (WebSockets) > 4.2 NATS Backplane (The Adapter)\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `tests/adapters/socket/test_nats_socket.py`\n- \u2705 Write tests using testcontainers-nats to verify Pub/Sub.\n- \u2705 Create `src/framework_m/adapters/socket/nats_socket.py`\n- \u2705 Implement `NatsSocketAdapter`:\n- \u2705 On Boot: Subscribe to `framework.events.*`.\n- \u2705 On Event: `await nc.publish(subject, json.dumps(msg).encode())`.\n- \u2705 On Receive (Sub): Forward to local connected Websocket clients (Connection Manager).\n\n### 4. Real-time Events (WebSockets) > 4.3 WebSocket Endpoint (Litestar)\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/web/socket.py`\n- \u2705 Implement `ConnectionManager`:\n- \u2705 Store active connections `Dict[UserId, List[WebSocket]]`.\n- \u2705 Add route `WS /api/v1/stream`:\n- \u2705 Authenticate user (Query param token).\n- \u2705 Upgrade connection.\n- \u2705 Register with `ConnectionManager`.\n- \u2705 Listen for client disconnect.\n\n### 7. Testing > 7.1 Unit Tests\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Test job registration:\n- \u2705 Test `@job` decorator\n- \u2705 Test job registry\n- \u2705 Test event bus:\n- \u2705 Test publish/subscribe\n- \u2705 Test event serialization\n\n### 7. Testing > 7.2 Integration Tests\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Test job execution:\n- \u2705 Enqueue job\n- \u2705 Verify job runs\n- \u2705 Check job result\n- \u2705 Test scheduled jobs:\n- \u2705 Register cron job\n- \u2705 Verify execution at scheduled time (use time mocking)\n- \u2705 Test webhooks:\n- \u2705 Create webhook\n- \u2705 Trigger event\n- \u2705 Verify HTTP call made\n- \u2705 Check webhook log\n- \u2705 Use testcontainers for NATS\n\n### 8. CLI Commands\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Add job management commands:\n- \u2705 `m worker` - Start worker process\n- \u2705 `m job:list` - List registered jobs\n- \u2705 `m job:run ` - Run job immediately\n- \u2705 `m job:status ` - Check job status\n\n### Validation Checklist\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Jobs can be enqueued and executed\n- \u2705 Scheduled jobs run at correct times\n- \u2705 Events are published and received\n- \u2705 Webhooks are triggered on events\n- \u2705 Job logs are created and updated\n- \u2705 Worker process starts and processes jobs\n\n---\n\n## Phase 05: CLI & Developer Tools\n\n**Phase**: 05\n**Objective**: Build a comprehensive CLI tool (`m`) to replace Frappe's \"bench\", with commands for development, deployment, and management.\n**Status**: 97% Complete\n\n### 1. CLI Framework Setup\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/cli/main.py`\n- \u2705 Use `cyclopts` for CLI framework (native async, better type support)\n- \u2705 Setup main CLI app:\n- \u2705 Add version command:\n- \u2705 Add help documentation for all commands\n\n### 1. CLI Framework Setup > 1.1 Pluggable CLI Architecture (Entry Points)\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 Define Entry Point Group: `framework_m.cli_commands`\n- \u2705 Implement Plugin Loader in `plugin_loader.py`:\n- \u2705 Use `importlib.metadata.entry_points(group=\"framework_m.cli_commands\")`\n- \u2705 Iterate over registered entry points\n- \u2705 If entry point is a `cyclopts.App`: `app.command(plugin_app, name=ep.name)`\n- \u2705 If entry point is a function: `app.command(plugin_func, name=ep.name)`\n- \u2705 Goal: 3rd party apps (and `framework-m-studio`) can add commands to `m` without modifying core.\n- \u2705 **Customization Philosophy**:\n- \u2705 Standard commands (`start`, `test`) are strict relays.\n- \u2705 **For customization** (e.g., `start-mqtt`, `super-test`): Users MUST register a custom command via entry points.\n- \u2705 **Result**: `m` remains a clean, standard runner.\n\n### 2. Development Server (Uvicorn Relay) > 2.1 Start Command\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m prod`:\n- \u2705 **Implementation**: Wrapper around `uvicorn`.\n- \u2705 **Argument Forwarding**: Uses cyclopts Parameter for options.\n- \u2705 **Usage**: `m prod --workers 4 --log-level debug`\n- \u2705 **Value Add**: Sets `PYTHONPATH`, auto-detects app, transparently passes args.\n\n### 2. Development Server (Uvicorn Relay) > 2.2 Worker Command (Taskiq Relay)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m worker`:\n- \u2705 **Implementation**: Native Taskiq integration (better than wrapper).\n- \u2705 **Options**: `--concurrency`, `--verbose`.\n- \u2705 **Usage**: `m worker --concurrency 8`\n\n### 2. Development Server (Uvicorn Relay) > 2.3 Studio Command (Litestar Relay)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m studio`:\n- \u2705 **Implementation**: Wrapper around `uvicorn` for Studio app.\n- \u2705 **Options**: `--host`, `--port`, `--reload`, `--log-level`.\n- \u2705 **Usage**: `m studio --port 9000 --reload`\n\n### 3. Database Commands (Alembic Relay) > 3.1 Migration Commands\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m migrate`:\n- \u2705 **Implementation**: Wrapper around MigrationManager (uses Alembic).\n- \u2705 **Commands**: `migrate`, `create`, `rollback`, `status`, `history`, `init`.\n- \u2705 **Usage**: `m migrate create \"add users\" --autogenerate`\n- \u2705 **Value Add**: Auto-detects schema changes, supports custom DB URL, env vars.\n\n### 4. Scaffolding (Cruft/Cookiecutter Relay) > 4.1 New App (Starter Template)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m new:app`:\n- \u2705 **Implementation**: Wrapper around `cruft` (cookiecutter).\n- \u2705 **Options**: `--template`, `--checkout`, `--no-input`.\n- \u2705 **Usage**: `m new:app myapp --checkout main`\n\n### 4. Scaffolding (Cruft/Cookiecutter Relay) > 4.2 New DocType (Jinja Relay)\n\n**Progress**: 26/26 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m new:doctype `:\n- \u2705 **Implementation**: Template rendering (embedded templates).\n- \u2705 **Output**: `doctype.py`, `controller.py`, `test_*.py`, `__init__.py`.\n- \u2705 **App Detection**: `--app` > pyproject.toml > interactive prompt.\n- \u2705 **CLI Parameter (Unattended/Automation)**:\n- \u2705 `--app `: Explicit app target.\n- \u2705 If `--app` provided, use it directly (no prompt).\n- \u2705 **Automation Friendly**: Scripts can pass `--app` to avoid prompts.\n- \u2705 **Auto-Detection from CWD**:\n- \u2705 If `--app` not provided, attempt to detect from current directory.\n- \u2705 Check if CWD is inside an app's source tree.\n- \u2705 Look for `pyproject.toml` in CWD or parent directories.\n- \u2705 Read `[project] name` to determine app.\n- \u2705 **Interactive Prompt (Default Fallback)**:\n- \u2705 If detection fails AND stdin is interactive (TTY):\n- \u2705 List installed apps from `framework_m.apps` entry points.\n- \u2705 Prompt user to select: `\"Which app? [my_app, other_app]\"`.\n- \u2705 If stdin is NOT interactive (pipe/automation):\n- \u2705 **Fail with clear error**: `\"Cannot detect app. Use --app .\"`.\n- \u2705 Exit code 1 (enables CI/automation to catch failures).\n- \u2705 **Implementation**: check `require_app()` in `new.py`\n- \u2705 **Scaffold Output Location**:\n- \u2705 Files created in: `{cwd}/doctypes/{doctype_name}/`\n- \u2705 Structure: `__init__.py`, `doctype.py`, `controller.py`, `test_*.py`\n- \u2705 **Templates**:\n- \u2705 Embedded templates in `new.py` (no external Jinja2 files needed).\n\n### 5. Testing & Quality (Standard Tool Relays) > 5.1 Test Relay\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m test`:\n- \u2705 **Command**: `pytest`\n- \u2705 **Options**: `--verbose`, `--coverage`, `-k`.\n- \u2705 **Usage**: `m test -k \"user\" --verbose`\n- \u2705 **Value Add**: Configures `PYTHONPATH`.\n\n### 5. Testing & Quality (Standard Tool Relays) > 5.2 Lint/Format Relay\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m lint`:\n- \u2705 **Command**: `ruff check . --fix`\n- \u2705 Implement `m format`:\n- \u2705 **Command**: `ruff format .`\n- \u2705 Implement `m typecheck`:\n- \u2705 **Command**: `mypy .`\n\n### 6. Configuration Management > 6.1 Config File\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `framework_config.toml` structure:\n- \u2705 Supports `[framework]`, `[apps]` sections\n- \u2705 Load/save with TOML format\n- \u2705 Auto-detect in CWD or parent directories\n\n### 6. Configuration Management > 6.2 Config Commands\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m config:show`: Display current config.\n- \u2705 Implement `m config:set `: Update config.\n\n### 7. Utility Relays > 7.1 Console (IPython Relay)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m console`:\n- \u2705 **Implementation**: Wrapper around `ipython` or `python -m asyncio`.\n- \u2705 **Value Add**: Pre-imports framework, async support.\n- \u2705 **Options**: `--no-ipython` for plain Python.\n\n### 7. Utility Relays > 7.2 System Info\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m info`:\n- \u2705 Show versions (Framework, Python).\n- \u2705 `--verbose` for platform and service info.\n\n### 7. Utility Relays > 7.3 Routes\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m routes`:\n- \u2705 Points to OpenAPI/Swagger docs.\n\n### 8. Build Commands > 8.1 Frontend Build\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m build` (placeholder):\n- \u2705 Relay to frontend build system (see Phase 09).\n- \u2705 Detects package.json and runs `npm run build`.\n\n### 8. Build Commands > 8.2 Docker Build\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Implement `m build:docker`:\n- \u2705 **Command**: Wrapper around `docker build`.\n- \u2705 **Value Add**: Reads image name/tag from `framework_config.toml`.\n- \u2705 **Options**: `--tag`, `--file`, `--no-cache`, `--push`.\n\n### 9. Pluggable Extensions\n\n**Progress**: 1/4 (25%)\n\n**Completed:**\n\n- \u2705 Plugin loader implemented (`load_plugins()`)\n\n**Pending:**\n\n- \u23f3 `m studio` (external)\n- \u23f3 `m docs:generate` (external)\n- \u23f3 `m codegen` (external)\n\n### 10. CLI Entry Point\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Configure `pyproject.toml` scripts: `m = \"framework_m.cli.main:app\"`\n- \u2705 Test installation (`m --help`) - 22 commands registered.\n\n---\n\n## Phase 06: Built-in DocTypes & Core Features\n\n**Phase**: 06\n**Objective**: Implement essential built-in DocTypes (User, Role, Permission) and core features like file attachments, audit logging, and printing.\n**Status**: 99% Complete\n\n### 1. Identity & Access Management (PIM) > 1.1 Identity Provider Protocol\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `IdentityProtocol` (Interface):\n- \u2705 `get_user(id: str) -> User`\n- \u2705 `authenticate(credentials) -> Token`\n- \u2705 `get_attributes(user) -> dict[str, Any]` (ABAC replacement for get_roles)\n\n### 1. Identity & Access Management (PIM) > 1.2 User DocTypes (Pluggable)\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 **Mode A: Local (Indie)**:\n- \u2705 `LocalUser` (SQL Table): Stores email, password_hash, full_name.\n- \u2705 `LocalIdentityAdapter`: argon2 password hashing, JWT generation.\n- \u2705 **Mode B: Federated (Enterprise)**:\n- \u2705 `FederatedIdentityAdapter` (Proxy):\n- \u2705 No SQL Table required.\n- \u2705 Hydrates from Auth Header (`X-User-ID`, `X-Email`) via `hydrate_from_headers()`.\n- \u2705 `UserPreferences` DocType stores settings locally.\n\n### 1. Identity & Access Management (PIM) > 1.3 User Controller (Adapter)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Implement `UserManager`:\n- \u2705 Delegates to configured `IdentityProvider` via `IdentityProtocol`.\n- \u2705 Abstraction layer: `user = await user_manager.get(id)`.\n- \u2705 Additional `create()` method for Indie mode with password hashing.\n\n### 1. Identity & Access Management (PIM) > 1.4 Auth API (Indie Mode)\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/auth/me`:\n- \u2705 Returns current user context (or 401).\n- \u2705 Create `POST /api/v1/auth/login` (Indie Only):\n- \u2705 Accepts `username`, `password`.\n- \u2705 Validates against `LocalUser` via `UserManager.authenticate()`.\n- \u2705 Returns JWT token.\n- \u2705 Create `POST /api/v1/auth/logout`:\n- \u2705 Returns success message (JWT auth is client-side logout).\n\n### 1. Identity & Access Management (PIM) > 1.5 Authentication Strategies (Multi-Method)\n\n**Progress**: 24/24 (100%)\n\n**Completed:**\n\n- \u2705 Create `AuthenticationProtocol` (Interface):\n- \u2705 `supports(headers: Mapping) -> bool` \u2014 Can this strategy handle this request?\n- \u2705 `authenticate(headers: Mapping) -> UserContext | None` \u2014 Extract user from request.\n- \u2705 **Built-in Strategies**:\n- \u2705 `SessionCookieAuth` \u2014 Browser sessions via cookies.\n- \u2705 `BearerTokenAuth` \u2014 JWT/OAuth2 access tokens.\n- \u2705 `ApiKeyAuth` \u2014 `X-API-Key` header for scripts/integrations.\n- \u2705 `HeaderAuth` \u2014 Federated mode gateway header hydration.\n- \u2705 `BasicAuth` \u2014 HTTP Basic for CLI tools.\n- \u2705 **Strategy Chain** (`AuthChain`):\n- \u2705 Try each strategy in priority order.\n- \u2705 First successful match wins.\n- \u2705 Configurable order via `framework_config.toml` (`create_auth_chain_from_config`).\n- \u2705 **API Key DocType** (Indie Mode):\n- \u2705 `ApiKey` DocType:\n- \u2705 `key_hash: str` \u2014 Hashed API key (never store plaintext, excluded from serialization).\n- \u2705 `user_id: str` \u2014 Owner of the key.\n- \u2705 `name: str` \u2014 Human-readable label.\n- \u2705 `scopes: list[str]` \u2014 Optional permission scopes.\n- \u2705 `expires_at: datetime | None` \u2014 Expiration.\n- \u2705 `last_used_at: datetime | None` \u2014 Usage tracking.\n- \u2705 `POST /api/v1/auth/api-keys` \u2014 Create new key.\n- \u2705 `DELETE /api/v1/auth/api-keys/{id}` \u2014 Revoke key.\n- \u2705 `GET /api/v1/auth/api-keys` \u2014 List user's keys.\n\n### 1. Identity & Access Management (PIM) > 1.6 Social Login (OAuth2/OIDC)\n\n**Progress**: 26/27 (96%)\n\n**Completed:**\n\n- \u2705 **Option A: Built-in OAuth2 (Indie)**:\n- \u2705 Supported Providers (config structure ready):\n- \u2705 Google (well-known URLs configured)\n- \u2705 GitHub (well-known URLs configured)\n- \u2705 Microsoft (well-known URLs configured)\n- \u2705 Generic OIDC (any compliant provider via `get_oidc_well_known`)\n- \u2705 Configuration via `framework_config.toml`:\n- \u2705 Endpoints:\n- \u2705 `GET /api/v1/auth/oauth/{provider}/start` \u2014 Redirect to provider.\n- \u2705 `GET /api/v1/auth/oauth/{provider}/callback` \u2014 Handle callback. _(Placeholder)_\n- \u2705 **Option B: Federated (Enterprise)**:\n- \u2705 Delegate to Keycloak / Authelia / Auth0 via `HeaderAuth` strategy.\n- \u2705 Framework receives headers only (`X-User-ID`, `X-Email`).\n- \u2705 Zero PII stored locally.\n- \u2705 **SocialAccount DocType**:\n- \u2705 `provider: str` \u2014 OAuth provider name.\n- \u2705 `provider_user_id: str` \u2014 Unique ID from provider.\n- \u2705 `user_id: str` \u2014 Links to local user.\n- \u2705 `email: str | None` \u2014 For lookup (optional).\n- \u2705 `display_name: str` \u2014 For UI display.\n- \u2705 One User can have multiple SocialAccounts.\n- \u2705 **Passwordless Option** _(Requires Section 10: Email)_:\n- \u2705 `POST /api/v1/auth/magic-link` \u2014 Send email with login link.\n- \u2705 `GET /api/v1/auth/magic-link/{token}` \u2014 Verify and create session.\n- \u2705 `MagicLinkCredentials` added to identity protocol\n- \u2705 Token generation with HMAC-SHA256 signing\n\n**Pending:**\n\n- \u23f3 Use `authlib` for OAuth2 flows. _(Full implementation deferred)_\n\n### 1. Identity & Access Management (PIM) > 1.7 Session Management\n\n**Progress**: 20/20 (100%)\n\n**Completed:**\n\n- \u2705 **Session Storage**:\n- \u2705 **Redis (Default)**: `RedisSessionAdapter` with JSON storage and TTL.\n- \u2705 **Database (Fallback)**: `DatabaseSessionAdapter` for indie apps.\n- \u2705 Configurable via `framework_config.toml`:\n- \u2705 **Session DocType** (database backend):\n- \u2705 `session_id: str` \u2014 Unique session identifier.\n- \u2705 `user_id: str` \u2014 Links to user.\n- \u2705 `expires_at: datetime`\n- \u2705 `ip_address: str | None`\n- \u2705 `user_agent: str | None`\n- \u2705 **SessionProtocol Interface**:\n- \u2705 `create()` \u2014 Create new session\n- \u2705 `get()` \u2014 Retrieve by ID\n- \u2705 `delete()` \u2014 Remove session\n- \u2705 `delete_all_for_user()` \u2014 Logout all\n- \u2705 `list_for_user()` \u2014 List active sessions\n- \u2705 **Session API**:\n- \u2705 `GET /api/v1/auth/sessions` \u2014 List active sessions.\n- \u2705 `DELETE /api/v1/auth/sessions/{id}` \u2014 Revoke session.\n- \u2705 `DELETE /api/v1/auth/sessions` \u2014 Logout all.\n\n### 1. Identity & Access Management (PIM) > 1.8 PII Handling (Indie Guidance)\n\n**Progress**: 16/16 (100%)\n\n**Completed:**\n\n- \u2705 **Auth Mode Presets** (`AuthMode` enum in `core/pii.py`):\n- \u2705 **Minimal PII Pattern**:\n- \u2705 `LocalUser` stores only:\n- \u2705 `email` (for login/notification)\n- \u2705 `display_name` (for UI)\n- \u2705 `password_hash` (only if `mode = \"local\"`)\n- \u2705 **Do NOT store by default** (`PII_SENSITIVE_FIELDS`):\n- \u2705 Full legal name\n- \u2705 Phone number\n- \u2705 Address\n- \u2705 Date of birth\n- \u2705 Helper: `is_sensitive_pii(field_name)` to detect sensitive fields.\n- \u2705 **Data Deletion** (`DeletionMode` enum):\n- \u2705 `DELETE /api/v1/auth/me` \u2014 Delete user account (GDPR right to erasure).\n- \u2705 `GET /api/v1/auth/me/data` \u2014 Export user data (GDPR data portability).\n- \u2705 Configurable: Hard delete vs anonymize.\n\n### 2. Tenancy & Attributes (Multi-Tenant Core) > 2.1 Tenant Protocol\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `TenantProtocol` (Interface in `core/interfaces/tenant.py`):\n- \u2705 `get_current_tenant() -> str`\n- \u2705 `get_tenant_attributes(tenant_id) -> dict`\n- \u2705 `TenantContext` model for request context\n- \u2705 Configuration helpers: `is_multi_tenant()`, `get_default_tenant_id()`\n\n### 2. Tenancy & Attributes (Multi-Tenant Core) > 2.2 Tenancy Adapters\n\n**Progress**: 12/12 (100%)\n\n**Completed:**\n\n- \u2705 **Mode A: Single Tenant (Indie)**:\n- \u2705 `ImplicitTenantAdapter` (`adapters/tenant.py`):\n- \u2705 `tenant_id=\"default\"` (configurable).\n- \u2705 `attributes={\"plan\": \"unlimited\", \"features\": \"*\"}`.\n- \u2705 `get_context()` returns `TenantContext` with `is_default=True`.\n- \u2705 Factory: `create_tenant_adapter_from_headers()` auto-selects.\n- \u2705 **Mode B: Multi-Tenant (Enterprise)**:\n- \u2705 `HeaderTenantAdapter` (`adapters/tenant.py`):\n- \u2705 Extracts `X-Tenant-ID` from Gateway headers.\n- \u2705 Extracts `X-Tenant-Attributes` (JSON) for feature flags, plan.\n- \u2705 `get_context()` parses headers into `TenantContext`.\n- \u2705 **Benefit**: Feature toggling per tenant without DB lookup.\n\n### 2. Tenancy & Attributes (Multi-Tenant Core) > 2.3 Attributes (The New Roles)\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 **Goal**: Replace \"Roles\" with refined \"Attributes\" (ABAC).\n- \u2705 **Implementation** (in `UserContext`):\n- \u2705 `user.attributes` (dict): Stores `{\"department\": \"sales\", \"level\": 5, \"roles\": [\"admin\"]}`.\n- \u2705 `user.get_attribute(key, default)` \u2014 Get attribute value.\n- \u2705 `user.has_attribute(key, value)` \u2014 Check attribute equality.\n- \u2705 **Legacy Support**: `user.has_role(\"x\")` checks both `roles` list AND `attributes.roles`.\n- \u2705 `is_system_user` uses `has_role(\"System\")` for consistency.\n\n### 3. Permission Management (Pluggable) > 3.1 Permission Protocol\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `PermissionProtocol` (`core/interfaces/permission.py`):\n- \u2705 `PolicyEvaluateRequest` - Stateless authorization request\n- \u2705 `PolicyEvaluateResult` - Result with authorized, decision_source, reason\n- \u2705 `async def evaluate(request) -> PolicyEvaluateResult`\n- \u2705 `async def get_permitted_filters(...) -> dict` - RLS support\n- \u2705 `PermissionAction` enum (read, write, create, delete, submit, cancel, amend)\n- \u2705 `DecisionSource` enum (rbac, abac, rebac, combo, custom)\n\n### 3. Permission Management (Pluggable) > 3.2 Permission Adapters\n\n**Progress**: 18/18 (100%)\n\n**Completed:**\n\n- \u2705 **Mode A: Standard (Indie)** - `RbacPermissionAdapter`:\n- \u2705 Reads permissions from `DocType.Meta.permissions`.\n- \u2705 Checks `CustomPermission` rules from database.\n- \u2705 Role matching against `principal_attributes[\"roles\"]`.\n- \u2705 Admin role bypass (configurable).\n- \u2705 RLS via `get_permitted_filters()` using `Meta.rls_field`.\n- \u2705 **Mode B: Citadel Policy (Enterprise)** - `CitadelPolicyAdapter`:\n- \u2705 `AuthorizationRequest` model (Cedar-compatible format):\n- \u2705 `Principal`: `user.id`\n- \u2705 `Action`: `doctype:action` (e.g., `Invoice:create`)\n- \u2705 `Resource`: `doctype:name` (e.g., `Invoice:INV-001`)\n- \u2705 `TenantID`: `user.tenant_id`\n- \u2705 `Context`: `doc.as_dict()`\n- \u2705 `PrincipalAttributes`: `user.attributes` (Diet Claims)\n- \u2705 `AuthorizationResponse` model with `allowed`, `reason`, `policy_id`\n- \u2705 Calls configurable policy endpoint (Cedar/OPA)\n- \u2705 Fallback behavior on error (configurable)\n- \u2705 Configuration via `[permissions.citadel]` in `framework_config.toml`\n\n### 4. File Management > 4.1 File DocType\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/doctypes/file.py`\n- \u2705 Define `File` DocType:\n- \u2705 `name: str` - File ID (inherited from BaseDocType)\n- \u2705 `file_name: str` - Original filename\n- \u2705 `file_url: str` - Storage URL\n- \u2705 `file_size: int` - Size in bytes\n- \u2705 `content_type: str` - MIME type\n- \u2705 `attached_to_doctype: str | None`\n- \u2705 `attached_to_name: str | None`\n- \u2705 `is_private: bool` - Private/public file (default: True)\n\n### 4. File Management > 4.2 File Upload API\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `POST /api/v1/file/upload` (`adapters/web/file_routes.py`):\n- \u2705 Accept multipart/form-data via `UploadFile`\n- \u2705 Validate file size (max 10MB default, configurable)\n- \u2705 Validate file type (configurable whitelist)\n- \u2705 Generate unique storage path (YYYY/MM/DD/random_filename)\n- \u2705 Create File DocType record\n- \u2705 Return file URL and metadata\n- \u2705 Configuration via `[files]` in `framework_config.toml`\n- \u2705 Response models: `FileUploadResponse`, `FileDeleteResponse`\n\n### 4. File Management > 4.3 File Download API\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/file/{file_id}`:\n- \u2705 Check permissions for private files (RLS on owner)\n- \u2705 Prepare streaming response from storage\n- \u2705 Set correct `Content-Type` header\n- \u2705 Set `Content-Disposition` header (inline/attachment)\n- \u2705 Support `?inline=true` for browser display\n- \u2705 Create `GET /api/v1/file/{file_id}/info`:\n- \u2705 Return metadata without downloading content\n\n### 4. File Management > 4.4 Storage Adapters\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Implement `LocalStorageAdapter` (`adapters/storage/local.py`):\n- \u2705 Save files to local directory with atomic writes\n- \u2705 Organize by date (YYYY/MM/DD) via path parameter\n- \u2705 Generate unique filenames (handled by file_routes)\n- \u2705 Path traversal protection\n- \u2705 Async operations via `aiofiles`\n- \u2705 Implement `S3StorageAdapter` (`adapters/storage/s3.py`):\n- \u2705 Use `aioboto3` for async S3 operations\n- \u2705 Presigned URLs for direct client uploads (PUT method)\n- \u2705 Multipart upload for large files (>25MB)\n- \u2705 Configure bucket and region from `framework_config.toml`\n- \u2705 Support S3-compatible services (MinIO, DigitalOcean Spaces)\n- \u2705 `InMemoryStorageAdapter` for testing (`adapters/storage/memory.py`)\n\n### 5. Audit Logging (Pluggable) > 5.1 Audit Log Protocol\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `AuditLogProtocol` (`core/interfaces/audit.py`):\n- \u2705 `AuditEntry` model (user, action, doctype, document_id, changes, metadata)\n- \u2705 `async def log(...)` - Record audit entry\n- \u2705 `async def query(...)` - Query with filters/pagination\n- \u2705 `InMemoryAuditAdapter` for testing\n\n### 5. Audit Logging (Pluggable) > 5.2 Audit Adapters\n\n**Progress**: 6/7 (86%)\n\n**Completed:**\n\n- \u2705 **Mode A: Database (Indie)**:\n- \u2705 `DatabaseAuditAdapter`: Writes to `ActivityLog` records.\n- \u2705 Uses ActivityLog DocType from 5.3\n- \u2705 **Mode B: External (Enterprise)**:\n- \u2705 `FileAuditAdapter`: Writes to `audit.log` (JSONL) for Splunk/Filebeat.\n- \u2705 Prevents main DB bloat.\n\n**Pending:**\n\n- \u23f3 `ElasticAuditAdapter`: Writes directly to Elasticsearch (Phase 10).\n\n### 5. Audit Logging (Pluggable) > 5.3 Activity Log DocType (Indie Only)\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/doctypes/activity_log.py`\n- \u2705 Define `ActivityLog` DocType:\n- \u2705 `user_id: str`\n- \u2705 `action: str` (create, read, update, delete)\n- \u2705 `doctype: str`\n- \u2705 `document_id: str`\n- \u2705 `timestamp: datetime`\n- \u2705 `changes: dict | None`\n- \u2705 `metadata: dict | None`\n- \u2705 Immutable (no write/delete permissions)\n\n### 5. Audit Logging (Pluggable) > 5.4 Activity Feed API\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/activity`:\n- \u2705 List recent activities\n- \u2705 Filter by user, doctype, document, action\n- \u2705 Paginate results (limit, offset)\n\n### 6. Error Logging > 6.1 Error Log DocType\n\n**Progress**: 12/12 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/doctypes/error_log.py`\n- \u2705 Define `ErrorLog` DocType:\n- \u2705 `title: str` - Short error description\n- \u2705 `error_type: str` - Exception class name\n- \u2705 `error_message: str` - Full error message\n- \u2705 `traceback: str | None` - Full stack trace\n- \u2705 `request_url: str | None`\n- \u2705 `user_id: str | None`\n- \u2705 `request_id: str | None` - For log correlation\n- \u2705 `timestamp: datetime`\n- \u2705 `context: dict | None` - Additional context\n- \u2705 Admin-only permissions (immutable)\n\n### 6. Error Logging > 6.2 Error Logging Integration\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Add global exception handler (`adapters/web/error_handler.py`):\n- \u2705 Catch all unhandled exceptions\n- \u2705 Create ErrorLog entry with full context\n- \u2705 Return user-friendly ErrorResponse (no sensitive data)\n- \u2705 Log to console in dev mode (configurable)\n- \u2705 `create_error_handler()` factory for Litestar\n\n### 7. Printing & PDF Generation > 7.1 Print Format DocType\n\n**Progress**: 10/10 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/doctypes/print_format.py`\n- \u2705 Define `PrintFormat` DocType:\n- \u2705 `name: str` - Display name\n- \u2705 `doctype: str` - Target DocType\n- \u2705 `template: str` - Jinja2 template path\n- \u2705 `is_default: bool`\n- \u2705 `css: str | None` - Custom CSS\n- \u2705 `header_html: str | None`, `footer_html: str | None`\n- \u2705 `page_size: str` (A4, Letter, etc.)\n- \u2705 `orientation: str` (portrait, landscape)\n\n### 7. Printing & PDF Generation > 7.2 Jinja Print Adapter\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/print/jinja_adapter.py`\n- \u2705 Implement `JinjaPrintAdapter`:\n- \u2705 `async def render_html(doc, template) -> str`\n- \u2705 Load Jinja2 template\n- \u2705 Render with document context\n- \u2705 Return HTML string\n- \u2705 `async def render_html_string(template_string, doc) -> str`\n- \u2705 Custom filters: `currency`, `date`, `datetime`\n\n### 7. Printing & PDF Generation > 7.3 Gotenberg PDF Adapter\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/print/gotenberg_adapter.py`\n- \u2705 Implement `GotenbergPrintAdapter`:\n- \u2705 `async def html_to_pdf(html, page_size, orientation, ...) -> bytes`\n- \u2705 Send HTML to Gotenberg service\n- \u2705 Return PDF bytes\n- \u2705 `async def is_available() -> bool` - Health check\n- \u2705 `GotenbergConfig` with URL, timeout, page size, margins\n- \u2705 Page sizes: A4, Letter, Legal, A3, A5\n\n### 7. Printing & PDF Generation > 7.4 Print API\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/print/{doctype}/{id}`:\n- \u2705 Query parameters:\n- \u2705 `format` - pdf/html (default: pdf)\n- \u2705 `print_format` - Custom format name\n- \u2705 Load document (TODO: integrate with repository - phase 10)\n- \u2705 Check read permission (TODO: integrate with permission - phase 10)\n- \u2705 Render using JinjaPrintAdapter \u2192 GotenbergPrintAdapter\n- \u2705 Return PDF or HTML with appropriate headers\n- \u2705 Fallback to HTML if Gotenberg unavailable\n\n### 8. System Settings > 8.1 System Settings DocType\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/doctypes/system_settings.py`\n- \u2705 Define `SystemSettings` DocType (singleton):\n- \u2705 `name: str` - Always \"System Settings\" (frozen)\n- \u2705 `app_name: str` - Application display name\n- \u2705 `timezone: str` - Default timezone\n- \u2705 `date_format: str` - Date format string\n- \u2705 `time_format: str` - Time format string\n- \u2705 `language: str` - Default language code\n- \u2705 `enable_signup: bool` - Allow registration\n- \u2705 `session_expiry: int` - Session timeout in minutes\n- \u2705 `maintenance_mode: bool` - Admin-only access\n\n### 8. System Settings > 8.2 Settings API\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/settings`:\n- \u2705 Return system settings via `SettingsResponse`\n- \u2705 Cache aggressively (5 min Cache-Control header)\n- \u2705 `invalidate_settings_cache()` helper\n- \u2705 `set_settings_cache()` helper\n\n### 9. Notification System > 9.1 Notification DocType\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/doctypes/notification.py`\n- \u2705 Define `Notification` DocType:\n- \u2705 `user_id: str` - Recipient\n- \u2705 `subject: str`\n- \u2705 `message: str`\n- \u2705 `notification_type: str` (info, success, warning, error, etc.)\n- \u2705 `read: bool`\n- \u2705 `doctype: str | None` - Related DocType\n- \u2705 `document_id: str | None`\n- \u2705 `timestamp: datetime`\n- \u2705 `from_user: str | None`\n- \u2705 `metadata: dict | None`\n- \u2705 RLS on user_id field\n\n### 9. Notification System > 9.2 Notification API\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /api/v1/notifications`:\n- \u2705 List user's notifications\n- \u2705 Filter by read/unread\n- \u2705 Create `PATCH /api/v1/notifications/{id}/read` - Mark as read\n- \u2705 Create `DELETE /api/v1/notifications/{id}` - Delete notification\n- \u2705 Create WebSocket endpoint for real-time notifications:\n- \u2705 `WS /api/v1/notifications/stream` - Real-time notification stream\n- \u2705 `push_notification(user_id, notification)` - Push to connected clients\n- \u2705 `create_notification_websocket_router()` factory\n\n### 10. Email Integration > 10.1 Email Queue DocType\n\n**Progress**: 20/20 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/doctypes/email_queue.py`\n- \u2705 Define `EmailQueue` DocType:\n- \u2705 `to: list[str]` - Recipients\n- \u2705 `cc/bcc: list[str] | None`\n- \u2705 `subject: str`\n- \u2705 `body: str` - HTML body\n- \u2705 `text_body: str | None` - Plain text alt\n- \u2705 `status: str` - Queued/Sending/Sent/Failed/Cancelled\n- \u2705 `priority: str` - low/normal/high\n- \u2705 `error: str | None`\n- \u2705 `retry_count/max_retries: int`\n- \u2705 `queued_at/sent_at: datetime`\n- \u2705 `reference_doctype/reference_id` - for tracking\n- \u2705 Port-Adapter Pattern:\n- \u2705 `EmailQueueProtocol` (interface/port)\n- \u2705 `DatabaseEmailQueueAdapter` (default - uses EmailQueue DocType)\n- \u2705 `InMemoryEmailQueueAdapter` (testing)\n- \u2705 `queue_email()` helper function\n- \u2705 `configure_email_queue()` for swapping adapters\n- \u2705 Ready for `NotificationServiceAdapter` integration\n\n### 10. Email Integration > 10.2 Email Sender\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Create email sender protocol and adapters:\n- \u2705 `EmailSenderProtocol` (interface/port)\n- \u2705 `SMTPEmailSender` - SMTP delivery adapter\n- \u2705 `LogEmailSender` - Console logging (development)\n- \u2705 `SMTPConfig` - Configuration dataclass\n- \u2705 Create background job `EmailProcessor`:\n- \u2705 `process_queue()` - Process pending emails\n- \u2705 `process_single(queue_id)` - Process single email\n- \u2705 Update EmailQueue status\n- \u2705 Retry logic (placeholder)\n- \u2705 Add helper function:\n- \u2705 `queue_email()` - Queue for sending\n- \u2705 `send_queued_email()` - Process single email\n\n### 11. Testing > 11.1 Unit Tests\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Test User DocType validation (`core/doctypes/test_user.py`, `core/services/test_user_manager.py`)\n- \u2705 Test Role assignment (`adapters/auth/test_rbac_permission.py`)\n- \u2705 Test Permission checking (8 permission test files)\n- \u2705 Test File upload/download (`core/test_file.py`, `adapters/web/test_file_routes.py`)\n- \u2705 Test Print rendering (`core/test_print_format.py`, `adapters/print/test_*.py`)\n\n### 11. Testing > 11.2 Integration Tests\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Test user creation and login (`integration/test_api_endpoints.py`)\n- \u2705 Test permission enforcement (`core/test_object_level_permissions.py`)\n- \u2705 Test file storage (local and S3) (`adapters/storage/test_local.py`, `adapters/storage/test_s3.py`)\n- \u2705 Test PDF generation with Gotenberg (`adapters/print/test_gotenberg_adapter.py`)\n- \u2705 Test email sending (`adapters/email/test_sender_processor.py`)\n\n### Validation Checklist\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 User and Role management works\n- \u2705 Permissions are enforced correctly\n- \u2705 Files can be uploaded and downloaded\n- \u2705 Audit logs are created\n- \u2705 PDFs can be generated\n- \u2705 Emails can be sent\n\n---\n\n## Phase 07: Studio (Code Generation UI)\n\n**Phase**: 07\n**Objective**: Build a visual DocType builder (`framework-m-studio`) that generates Python code, running as a Litestar app.\n**Status**: 93% Complete\n\n### 1. Packaging Strategy (Runtime vs DevTools) > 1.1 framework-m (Runtime)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 **Goal**: Production-grade kernel.\n- \u2705 **Content**: API Server, ORM, DocType Engine, Basic CLI (`start`, `migrate`, `worker`).\n- \u2705 **Dependencies**: `litestar`, `sqlalchemy`, `pydantic`, `cyclopts`.\n- \u2705 **Excluded**: Code generators, Studio UI, Heavy formatting/linting libs.\n\n### 1. Packaging Strategy (Runtime vs DevTools) > 1.2 framework-m-studio (DevTools)\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 **Goal**: Developer Productivity & SaaS Component.\n- \u2705 **Content**: Studio UI, Code Generators, Visual Editors.\n- \u2705 **Dependencies**: `framework-m`, `libcst`, `jinja2`.\n- \u2705 **Integration**: Plugs into `m` CLI via entry points (`m codegen`).\n\n### 2. Studio Backend (Litestar App) > 1. Project Structure (Monorepo)\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `apps/studio/` in the workspace\n- \u2705 Initialize `apps/studio/pyproject.toml`:\n- \u2705 Add dependency: `framework-m = { workspace = true }`\n- \u2705 Add dependency: `libcst` (for code modification)\n- \u2705 Add dependency: `jinja2` (for code generation)\n- \u2705 Add code generation endpoints\n\n### 2. Studio Backend (Litestar App) > 1.2 File System API\n\n**Progress**: 14/14 (100%)\n\n**Completed:**\n\n- \u2705 Create `GET /studio/api/doctypes`:\n- \u2705 Scan project for `*.py` files containing DocType classes\n- \u2705 Return list of DocTypes with metadata\n- \u2705 Create `GET /studio/api/doctype/{name}`:\n- \u2705 Read DocType file\n- \u2705 Parse with LibCST\n- \u2705 Return structured JSON\n- \u2705 Create `POST /studio/api/doctype/{name}`:\n- \u2705 Accept DocType schema JSON\n- \u2705 Generate Python code\n- \u2705 Write to file system\n- \u2705 Create `DELETE /studio/api/doctype/{name}`:\n- \u2705 Delete DocType file\n- \u2705 Delete Controller file (if exists)\n\n### 2. LibCST Code Transformer > 2.1 Parser\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m_studio/codegen/parser.py`\n- \u2705 Implement `parse_doctype(file_path: str) -> dict`:\n- \u2705 Use LibCST to parse Python file\n- \u2705 Extract class definition\n- \u2705 Extract fields with types and defaults\n- \u2705 Extract Config metadata\n- \u2705 Return structured dict\n\n### 2. LibCST Code Transformer > 2.2 Generators (The Strategy)\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 **Creation (Scaffolding)**:\n- \u2705 Use `jinja2` templates (shared with CLI `m new:doctype`).\n- \u2705 Goal: Generate clean, standard Python code from scratch.\n- \u2705 Implement `generate_doctype_source(schema: dict) -> str`.\n- \u2705 **Mutation (Transformer)**:\n- \u2705 Use `LibCST` to parse and modify existing files.\n- \u2705 Goal: Add/Edit fields while preserving comments and custom methods.\n- \u2705 Implement `update_doctype_source(source: str, schema: dict) -> str`.\n\n### 2. LibCST Code Transformer > 2.3 Test Generator\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m_studio/codegen/test_generator.py`\n- \u2705 Implement `generate_test(schema: dict) -> str`:\n- \u2705 Generate `test_{doctype}.py`\n- \u2705 Import `pytest` and DocType\n- \u2705 Generate basic CRUD test (Create, Read, Update, Delete)\n- \u2705 Generate validation failure test (if required fields exist)\n\n### 2. LibCST Code Transformer > 2.4 Transformer\n\n**Progress**: 12/12 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m_studio/codegen/transformer.py`\n- \u2705 Implement `update_doctype(file_path: str, schema: dict)`:\n- \u2705 Parse existing file with LibCST\n- \u2705 Locate DocType class\n- \u2705 Update/add/remove fields\n- \u2705 Preserve comments and custom methods\n- \u2705 Write back to file\n- \u2705 Handle edge cases:\n- \u2705 Field renamed (detect and update)\n- \u2705 Field type changed\n- \u2705 Field deleted (remove from class)\n- \u2705 Custom methods preserved\n\n### 3. Studio Frontend > 3.1 React App Setup (Refine Stack)\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Create `apps/studio/studio_ui/` directory\n- \u2705 Initialize Refine app:\n- \u2705 Install dependencies per ADR-0005:\n- \u2705 Create Framework M Data Provider (`src/providers/dataProvider.ts`):\n- \u2705 Configure API base URLs (per ADR-0005):\n- \u2705 Read from `window.__FRAMEWORK_CONFIG__` or env vars\n- \u2705 Support same-origin, subdomain, and CDN scenarios\n- \u2705 Default to `/studio/api` for Studio endpoints\n\n### 3. Studio Frontend > 3.2 DocType List View (Refine Resource)\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `pages/doctypes/list.tsx`:\n- \u2705 Use `useList` hook from `@refinedev/core` to fetch DocTypes\n- \u2705 Display with `@tanstack/react-table` + Tailwind styling\n- \u2705 Add search/filter with `useTable` globalFilter\n- \u2705 Add \"New DocType\" button \u2192 navigates to `/doctypes/create`\n\n### 3. Studio Frontend > 3.3 DocType Editor (Refine Form)\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `pages/doctypes/edit.tsx`:\n- \u2705 Use `useOne`/`useUpdate`/`useCreate` hooks for data\n- \u2705 Left panel: Field list with `@dnd-kit/sortable`\n- \u2705 Right panel: Field properties form (name, type, required, default, description)\n- \u2705 Top bar: DocType name input + Save button\n\n### 3. Studio Frontend > 3.4 Field Editor (RJSF Schema-Driven)\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Create `components/FieldEditor.tsx`:\n- \u2705 Render with RJSF using JSON Schema for field properties\n- \u2705 Custom Tailwind widgets: `TextWidget`, `SelectWidget`, `CheckboxWidget`, `TextareaWidget`\n- \u2705 Dynamic \"Validators\" collapsible for min/max length, pattern, etc.\n- \u2705 Live preview of generated Python type annotation\n\n### 3. Studio Frontend > 3.5 Drag & Drop\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Implement field reordering:\n- \u2705 Use `@dnd-kit/core` + `@dnd-kit/sortable`\n- \u2705 Allow dragging fields to reorder\n- \u2705 Update schema on drop (via `arrayMove`)\n\n### 3. Studio Frontend > 3.6 Code Preview (Monaco Editor)\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `components/CodePreview.tsx`:\n- \u2705 Use `@monaco-editor/react` with `language=\"python\"`\n- \u2705 Read-only mode (`readOnly: true`)\n- \u2705 Theme: `vs-dark`\n- \u2705 Auto-update on schema change (memoized generation)\n- \u2705 Copy button for generated code\n\n### 3. Studio Frontend > 3.7 Save Flow\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement save functionality:\n- \u2705 Validate schema (name required)\n- \u2705 Call backend API via `useCreate`/`useUpdate`\n- \u2705 Navigate back to list on success\n- \u2705 Loading state on save button\n\n### 3. Studio Frontend > 3.8 Visual Data Modeling (Mind Map)\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement ERD/Mind Map View:\n- \u2705 Central Node: Current DocType (highlighted)\n- \u2705 Connected Nodes: Related DocTypes via Link fields\n- \u2705 Visual Action: Drag to connect nodes \u2192 Creates Link Field\n- \u2705 Using `@xyflow/react` (React Flow v12)\n\n### 3. Studio Frontend > 3.9 Layout Designer (Grid System)\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement Visual Layout Drag & Drop:\n- \u2705 Define Sections with title and Columns (1/2/3 cols selector)\n- \u2705 Drag fields from palette into grid cells\n- \u2705 Real-time form preview toggle\n- \u2705 Add/delete sections\n\n### 3. Studio Frontend > 3.10 Module Explorer\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement Module Scanner:\n- \u2705 Group DocTypes by directory/app module (tree structure)\n- \u2705 Tree view navigation in Sidebar\n- \u2705 Expand/collapse all, search filter\n- \u2705 Click to navigate to DocType editor\n\n### 4. Studio Cloud Mode (Ephemeral & Git-Backed) > 4.3 Git Adapter (Data Agnostic)\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m_studio/git/protocol.py` - GitAdapterProtocol (Port)\n- \u2705 Create `src/framework_m_studio/git/adapter.py` - GitAdapter (Adapter)\n- \u2705 Implement `GitAdapter`:\n- \u2705 Wraps standard `git` binary via asyncio subprocess.\n- \u2705 `clone(repo_url, auth)`: Clone to temp dir.\n- \u2705 `commit(message)`: Stage and commit changes.\n- \u2705 `push(branch)`: Push to remote.\n- \u2705 `pull()`: Pull latest changes.\n- \u2705 `create_branch(name)`: Create new branch.\n- \u2705 `get_status()`: Get workspace status.\n- \u2705 Unit tests in `tests/test_git_adapter.py` (12 tests)\n\n### 4. Studio Cloud Mode (Ephemeral & Git-Backed) > 4.4 GitHub Provider\n\n**Progress**: 9/9 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m_studio/git/github_provider.py`\n- \u2705 Implement `GitHubProvider`:\n- \u2705 Use GitHub API for operations (REST API with urllib)\n- \u2705 Support personal access tokens (Bearer auth)\n- \u2705 Handle authentication (GitHubAuthError)\n- \u2705 Create pull requests\n- \u2705 List pull requests\n- \u2705 Validate tokens\n- \u2705 Unit tests in `tests/test_github_provider.py` (9 tests)\n\n### 4. Studio Cloud Mode (Ephemeral & Git-Backed) > 4.5 Generic Git Provider\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Support HTTPS and SSH (via git CLI)\n- \u2705 Work with any Git server (generic implementation)\n\n### 4. Studio Cloud Mode (Ephemeral & Git-Backed) > 4.6 Workspace Management\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m_studio/workspace.py`\n- \u2705 Implement workspace lifecycle:\n- \u2705 Clone repo to temp directory\n- \u2705 Track workspace sessions\n- \u2705 Clean up old workspaces (TTL-based)\n- \u2705 Handle concurrent edits (asyncio lock)\n- \u2705 Unit tests in `tests/test_workspace.py` (12 tests)\n\n### 4. Studio Cloud Mode (Ephemeral & Git-Backed) > 4.7 Cloud Studio API\n\n**Progress**: 12/12 (100%)\n\n**Completed:**\n\n- \u2705 Create `POST /studio/api/workspace/connect`:\n- \u2705 Accept repo URL and token\n- \u2705 Clone repository\n- \u2705 Return workspace ID\n- \u2705 Create `POST /studio/api/workspace/{id}/commit`:\n- \u2705 Commit changes\n- \u2705 Push to branch\n- \u2705 Return commit SHA\n- \u2705 Create `GET /studio/api/workspace/{id}/status`: Git status\n- \u2705 Create `POST /studio/api/workspace/{id}/pull`: Pull latest\n- \u2705 Create `DELETE /studio/api/workspace/{id}`: Cleanup workspace\n- \u2705 Create `GET /studio/api/workspace/sessions`: List all sessions\n\n### 5. Studio CLI Command > 5.1 SPA Packaging & Serving (Path Prefix Architecture)\n\n**Progress**: 20/21 (95%)\n\n**Completed:**\n\n- \u2705 **Build Process**:\n- \u2705 `pnpm run build` in `studio_ui` outputs to `src/framework_m_studio/static/` (vite.config.ts).\n- \u2705 `pyproject.toml` includes `framework_m_studio/static/**/*`.\n- \u2705 **Serving Strategy (Litestar)** (in `app.py`):\n- \u2705 **UI Routes**: Serve `/studio/ui/*` \u2192 React SPA (HTML pages)\n- \u2705 **API Routes**: Serve `/studio/api/*` \u2192 JSON endpoints\n- \u2705 **Assets**: Serve `/studio/ui/assets/*` as static files (Cached)\n- \u2705 **SPA Catch-all**: `/studio/ui/{path:path}` \u2192 returns `index.html` for client-side routing\n- \u2705 **Redirect**: `/studio` \u2192 `/studio/ui/` for convenience\n- \u2705 **Content-Type Fix**: All responses use explicit `media_type` to prevent download issues\n- \u2705 **Implementation**: `serve_spa()` and `_get_spa_response()` functions in `app.py`\n- \u2705 Implement `m studio` (in `framework-m-studio` package):\n- \u2705 Register CLI command via entry point:\n- \u2705 Start uvicorn server on port 9000 (default)\n- \u2705 Serve Studio UI\n- \u2705 Options:\n- \u2705 `--port` - Custom port\n- \u2705 `--host` - Custom host\n- \u2705 `--reload` - Development mode\n- \u2705 `--cloud` - Enable cloud mode\n\n**Pending:**\n\n- \u23f3 Open browser automatically (TODO)\n\n### 6. DevTools CLI (Extensions) > 6.1 Documentation Generator (`m docs:generate`)\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Dependencies: Uses stdlib `urllib`, `json`, `subprocess` (no external deps required).\n- \u2705 Command: `m docs:generate`:\n- \u2705 **API Reference**: Generates markdown from DocTypes in `docs_generator.py`.\n- \u2705 **OpenAPI Export**: Exports `openapi.json` from URL (`--openapi-url`).\n- \u2705 **Site Build**: Runs `mkdocs build` if available (`--build-site`).\n- \u2705 Unit tests in `tests/test_docs_generator.py` (10 tests)\n\n### 6. DevTools CLI (Extensions) > 6.2 Client SDK Generator (`m codegen client`)\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Dependencies: Uses stdlib only (no external deps required).\n- \u2705 Command: `m codegen client`:\n- \u2705 **Flow**:\n- \u2705 **Arguments**:\n- \u2705 **TypeScript**: Generates interfaces + fetch client in `sdk_generator.py`.\n- \u2705 **Python**: Generates Pydantic models in `sdk_generator.py`.\n- \u2705 Unit tests in `tests/test_codegen.py` (6 tests)\n- \u2705 CLI tests updated in `tests/test_cli.py`\n\n### 7. Field Type Library > 7.1 Supported Field Types\n\n**Progress**: 13/13 (100%)\n\n**Completed:**\n\n- \u2705 Implement field type definitions:\n- \u2705 Text (str)\n- \u2705 Number (int, float)\n- \u2705 Checkbox (bool)\n- \u2705 Date (date)\n- \u2705 DateTime (datetime)\n- \u2705 Select (enum)\n- \u2705 Link (foreign key)\n- \u2705 Table (child table)\n- \u2705 Level 1: Grid/Table view\n- \u2705 Level 2+: Drill-down pattern (Button -> Drawer/Dialog) for infinite nesting\n- \u2705 JSON (dict)\n- \u2705 File (file upload)\n\n### 7. Field Type Library > 7.2 Custom Field Type Discovery\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 **Dynamic Loading**: Studio reads available types from `FieldRegistry` at runtime.\n- \u2705 API Endpoint: `GET /studio/api/field-types`\n- \u2705 Returns: Built-in types + Types registered by installed apps.\n- \u2705 Each type includes: `name`, `pydantic_type`, `sqlalchemy_type`, `ui_component` (optional).\n- \u2705 **UI Integration**: Field Type Selector in DocType Editor dynamically populates from this API.\n\n### 7. Field Type Library > 7.3 Custom UI Components (Client-Side)\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 **Goal**: Allow apps to register custom React components for their field types.\n- \u2705 **Registration (App-Side)**:\n- \u2705 **Discovery (Studio-Side)**:\n- \u2705 **Shadow Build** (Phase 09) loads all app `frontend/` plugins.\n- \u2705 Studio UI checks `window.__STUDIO_FIELD_COMPONENTS__` map.\n- \u2705 Falls back to default `` if no custom component.\n\n### 7. Field Type Library > 7.5 Live Preview & Sandbox Mode\n\n**Progress**: 0/11 (0%)\n\n**Pending:**\n\n- \u23f3 **Recommendation**: Default to feature branches for teams. Add branch selector to Studio UI.\n- \u23f3 **Goal**: Preview the DocType being edited with mock data AND simulate CRUD.\n- \u23f3 **Implementation**:\n- \u23f3 **Preview Tab**: Toggle between \"Schema Editor\" and \"Sandbox\".\n- \u23f3 **Mock Data Generation**:\n- \u23f3 Use `@faker-js/faker` or simple generators.\n- \u23f3 Generate sample rows based on field types.\n- \u23f3 **Form Preview**: Render `AutoForm` with generated mock doc.\n- \u23f3 **List Preview**: Render `AutoTable` with 5-10 mock rows.\n- \u23f3 **Schema Source**: Uses the _in-memory_ schema being edited, NOT the saved file.\n- \u23f3 Allows instant feedback without saving.\n\n### 7. Field Type Library > 7.6 Sandbox: Interactive CRUD Simulation\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 **Goal**: Let users test the full UX before committing the DocType.\n- \u2705 **Features**:\n- \u2705 **New**: Create a new mock document. Test mandatory field validation.\n- \u2705 **Update**: Edit an existing mock document. See form behavior.\n- \u2705 **Delete**: Delete mock document. See confirmation dialogs.\n- \u2705 **List (Few)**: Show 5 rows. Test basic list rendering.\n- \u2705 **List (Many)**: Paginate 100+ mock rows. Test performance.\n- \u2705 **Filter**: Apply filters and see results change.\n- \u2705 **Validation**: Submit form with missing required fields. See error messages.\n- \u2705 **Data Store**: In-memory array (resets on page reload).\n- \u2705 **Benefit**: No database required. Test UX before writing to disk.\n\n### 7. Field Type Library > 7.7 Local Hot Reload (Optional Enhancement)\n\n**Progress**: 0/8 (0%)\n\n**Pending:**\n\n- \u23f3 **Goal**: For users who want to test with a **real local database** instead of mock data.\n- \u23f3 **Prerequisites**: Local Postgres/SQLite + `m prod` running.\n- \u23f3 **Flow**:\n- \u23f3 Studio saves DocType to file.\n- \u23f3 File watcher detects change \u2192 triggers schema sync.\n- \u23f3 Table created/updated in local DB.\n- \u23f3 User can now test with real CRUD operations.\n- \u23f3 **Benefit**: Bridge between mock sandbox and production\u2014test with actual data locally.\n\n### 7. Field Type Library > 7.2 Field Metadata\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Define field options:\n- \u2705 Label\n- \u2705 Description\n- \u2705 Required\n- \u2705 Default value\n- \u2705 Validation rules\n- \u2705 Display options (hidden, read-only)\n\n### 8. Controller Scaffolding\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Add controller generation:\n- \u2705 Generate controller file template\n- \u2705 Add common hook methods\n- \u2705 Add validation examples\n- \u2705 Controller editor in UI:\n- \u2705 List hook methods\n- \u2705 Add custom methods\n- \u2705 Code editor for method bodies\n\n### 9. Testing > 9.1 Unit Tests\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Test LibCST parser:\n- \u2705 Parse valid DocType\n- \u2705 Extract fields correctly\n- \u2705 Handle edge cases\n- \u2705 Test code generator:\n- \u2705 Generate valid Python code\n- \u2705 Format correctly\n- \u2705 Handle all field types\n\n### 9. Testing > 9.2 Integration Tests\n\n**Progress**: 11/11 (100%)\n\n**Completed:**\n\n- \u2705 Test full flow:\n- \u2705 Create DocType via UI\n- \u2705 Save to file system\n- \u2705 Reload and verify\n- \u2705 Update DocType\n- \u2705 Verify changes preserved\n- \u2705 Test Git integration:\n- \u2705 Clone repository\n- \u2705 Make changes\n- \u2705 Commit and push\n- \u2705 Verify on GitHub\n\n### 10. Documentation\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create Studio user guide:\n- \u2705 How to start Studio\n- \u2705 How to create DocType\n- \u2705 How to add fields\n- \u2705 How to configure permissions\n- \u2705 How to use Git mode\n\n### Validation Checklist\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Studio UI can create and edit DocTypes\n- \u2705 Generated code is valid Python\n- \u2705 Changes are saved to file system\n- \u2705 LibCST preserves custom code\n- \u2705 Git mode can commit and push\n- \u2705 Studio works in both local and cloud modes\n\n---\n\n## Phase 08: Workflows & Advanced Features\n\n**Phase**: 08\n**Objective**: Implement pluggable workflow engine, DocType overrides, app-defined ports, and advanced extensibility features.\n**Status**: 100% Complete\n\n### 1. Workflow System > 1.1 Workflow Protocol\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/core/interfaces/workflow.py`\n- \u2705 Define `WorkflowProtocol`:\n- \u2705 `async def start_workflow(doctype: str, doc_id: str, workflow_name: str)`\n- \u2705 `async def get_workflow_state(doc_id: str) -> str`\n- \u2705 `async def transition(doc_id: str, action: str, user: UserContext)`\n- \u2705 `async def get_available_actions(doc_id: str, user: UserContext) -> list[str]`\n\n### 1. Workflow System > 1.2 Internal Workflow Adapter\n\n**Progress**: 28/28 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/workflow/internal_workflow.py`\n- \u2705 Create `WorkflowState` DocType:\n- \u2705 `name: str`\n- \u2705 `workflow: str` - Workflow name\n- \u2705 `doctype: str`\n- \u2705 `document_name: str`\n- \u2705 `current_state: str`\n- \u2705 `updated_at: datetime`\n- \u2705 Create `WorkflowTransition` DocType:\n- \u2705 `name: str`\n- \u2705 `workflow: str`\n- \u2705 `from_state: str`\n- \u2705 `to_state: str`\n- \u2705 `action: str` - Action name\n- \u2705 `allowed_roles: list[str]`\n- \u2705 `condition: str | None` - Python expression\n- \u2705 Create `Workflow` DocType:\n- \u2705 `name: str` - Workflow name\n- \u2705 `doctype: str` - Target DocType\n- \u2705 `initial_state: str`\n- \u2705 `states: list[dict]` - State definitions\n- \u2705 `transitions: list[WorkflowTransition]`\n- \u2705 Implement `InternalWorkflowAdapter`:\n- \u2705 Load workflow definition\n- \u2705 Validate transitions\n- \u2705 Check permissions\n- \u2705 Update state\n- \u2705 Trigger hooks / **Emit Event** (Side effects via Event Bus only)\n\n### 1. Workflow System > 1.3 Temporal Workflow Adapter (Optional)\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Create `src/framework_m/adapters/workflow/temporal_adapter.py`\n- \u2705 Implement `TemporalWorkflowAdapter`:\n- \u2705 Connect to Temporal server\n- \u2705 Start workflows\n- \u2705 Query workflow state\n- \u2705 Signal workflows for transitions\n\n### 2. DocType Overrides > 2.1 Override Registry\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Update `MetaRegistry`:\n- \u2705 Add `register_override(base_doctype: str, override_class: Type[BaseDocType])`\n- \u2705 When loading DocType, check for overrides\n- \u2705 Use override class if registered\n\n### 2. DocType Overrides > 2.2 Schema Extension\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Implement schema merging:\n- \u2705 Combine fields from base and override\n- \u2705 Override can add new fields\n- \u2705 Override can modify field properties\n- \u2705 Base fields cannot be removed\n\n### 2. DocType Overrides > 2.3 Table Alteration\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Update `SchemaMapper`:\n- \u2705 Detect schema changes from overrides\n- \u2705 Generate ALTER TABLE migrations\n- \u2705 Add new columns\n- \u2705 Modify column types (with caution)\n\n### 3. App-Defined Ports > 3.1 Custom Protocol Registration\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Update Container to support app-defined protocols:\n- \u2705 Allow apps to define custom protocols\n- \u2705 Register via entrypoints\n- \u2705 Other apps can override using `container.override()`\n\n### 4. Child Tables (Nested DocTypes) > 4.1 Child Table Support\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 Add `is_child` flag to DocType Config\n- \u2705 Child tables don't have independent routes\n- \u2705 Stored as JSON or separate table with parent reference\n\n### 4. Child Tables (Nested DocTypes) > 4.2 Implementation\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Update `SchemaMapper`:\n- \u2705 Create separate table for child\n- \u2705 Add `parent` and `parenttype` columns\n- \u2705 Add `idx` for ordering\n- \u2705 Update `GenericRepository`:\n- \u2705 Save child records when saving parent\n- \u2705 Delete old children and insert new ones\n- \u2705 Load children when loading parent\n\n### 5. Virtual Fields > 5.1 Computed Fields\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Add `@computed_field` decorator:\n- \u2705 Virtual fields not stored in database\n- \u2705 Computed on load\n- \u2705 Included in API responses\n\n### 6. Link Fields (Foreign Keys) > 6.1 Link Field Type\n\n**Progress**: 1/1 (100%)\n\n**Completed:**\n\n- \u2705 Add Link field type using json_schema_extra:\n\n### 6. Link Fields (Foreign Keys) > 6.2 Implementation\n\n**Progress**: 8/8 (100%)\n\n**Completed:**\n\n- \u2705 Update `SchemaMapper`:\n- \u2705 Detect link fields via json_schema_extra\n- \u2705 Create foreign key constraint to target table\n- \u2705 Reference target table's id column\n- \u2705 Add database enforcement:\n- \u2705 Foreign key constraints enforce referential integrity\n- \u2705 Works with SQLite and PostgreSQL (database-agnostic)\n- \u2705 SQLite foreign keys enabled via PRAGMA\n\n### 6. Link Fields (Foreign Keys) > 6.3 Link Fetching\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Add `fetch_from` option:\n- \u2705 Auto-populate field from linked document\n- \u2705 Update GenericRepository to fetch values before save\n- \u2705 Fetch values automatically on insert and update\n- \u2705 Handle null links gracefully\n- \u2705 Support multiple fetch_from fields\n\n### 7. Naming Series (Human-Readable Names) > 7.1 Auto-Naming Configuration\n\n**Progress**: 7/7 (100%)\n\n**Completed:**\n\n- \u2705 Add naming configuration to DocType:\n- \u2705 Implement naming patterns:\n- \u2705 `.YYYY.` - Year (2026)\n- \u2705 `.MM.` - Month (01-12)\n- \u2705 `.DD.` - Day (01-31)\n- \u2705 `.####` - Sequential number with padding (0001, 0002...)\n- \u2705 `{field}` - Field value from entity\n\n### 7. Naming Series (Human-Readable Names) > 7.2 Implementation (Optimistic Approach)\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Name generation happens AFTER insert (id already assigned):\n- \u2705 For high-volume DocTypes, use PostgreSQL sequences:\n\n### 7. Naming Series (Human-Readable Names) > 7.3 Counter Storage\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `NamingCounter` DocType:\n- \u2705 `prefix: str` - e.g., \"INV-2024-\"\n- \u2705 `current: int` - Current counter value\n- \u2705 **Note**: No row-level locking needed. Use optimistic update with retry.\n\n### 8. Validation Rules > 8.1 Field Validators\n\n**Progress**: 1/1 (100%)\n\n**Completed:**\n\n- \u2705 Add Pydantic validators:\n\n### 8. Validation Rules > 8.2 Document Validators\n\n**Progress**: 1/1 (100%)\n\n**Completed:**\n\n- \u2705 Use controller `validate()` hook:\n\n### 9. Testing > 9.1 Unit Tests\n\n**Progress**: 5/5 (100%)\n\n**Completed:**\n\n- \u2705 Test workflow transitions (15 tests in `test_workflow.py`)\n- \u2705 Test DocType overrides (16 tests in `test_meta_registry_overrides.py`)\n- \u2705 Test child table operations (38 tests in `test_child_tables.py`)\n- \u2705 Test link field validation (16 tests in `test_link_fields.py`)\n- \u2705 Test naming series (29 tests in `test_naming_series.py` + validation tests)\n\n### 9. Testing > 9.2 Integration Tests\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Test full workflow lifecycle (6 tests in `test_workflow_lifecycle.py`)\n- \u2705 Test override schema migration (10 tests in `test_override_migration.py`)\n- \u2705 Test child table CRUD (12 tests in `test_child_table_integration.py`)\n- \u2705 Test app-defined ports (9 tests in `test_app_ports.py`)\n\n### Validation Checklist\n\n**Progress**: 6/6 (100%)\n\n**Completed:**\n\n- \u2705 Workflows can be defined and executed (15 unit + 6 integration tests)\n- \u2705 DocTypes can be extended via overrides (16 unit + 10 integration tests)\n- \u2705 Child tables work correctly (38 unit + 12 integration tests)\n- \u2705 Link fields create proper foreign keys (16 tests in `test_link_fields.py`)\n- \u2705 Naming series generates unique names (29 tests in `test_naming_series.py`)\n- \u2705 App-defined ports can be registered (9 tests in `test_app_ports.py`)\n\n---\n\n## Phase 09A: Frontend\n\n**Phase**: 09a\n**Objective**: Build a generic admin UI (the \"Desk\") that renders forms and lists from metadata.\n**Status**: 97% Complete\n\n### 1. Frontend Architecture > 1.2 Project Setup\n\n**Progress**: 1/6 (17%)\n\n**Completed:**\n\n- \u2705 **Option A: Use Refine CLI (Recommended)**\n\n**Pending:**\n\n- \u23f3 **Option B: Manual Setup (Full Control)**\n- \u23f3 Install form libraries (RJSF):\n- \u23f3 Install UI layer (shadcn/ui):\n- \u23f3 Install dashboard/charts (Tremor):\n- \u23f3 Install Studio-specific tools:\n\n### 1. Frontend Architecture > 1.3 Refine Data Provider\n\n**Progress**: 4/4 (100%)\n\n**Completed:**\n\n- \u2705 Create `frameworkMDataProvider.ts`:\n- \u2705 Implements Refine's `DataProvider` interface\n- \u2705 Maps to Framework M REST API (`/api/v1/{resource}`)\n- \u2705 Handles pagination, sorting, filtering\n\n### 1. Frontend Architecture > 1.4 Base URL Configuration\n\n**Progress**: 2/2 (100%)\n\n**Completed:**\n\n- \u2705 Support configurable base URLs for different deployment scenarios:\n- \u2705 Inject configuration via `window.__FRAMEWORK_CONFIG__`:\n\n### 1. Frontend Architecture > 1.5 App Entry Point\n\n**Progress**: 1/1 (100%)\n\n**Completed:**\n\n- \u2705 Create Refine app entry (`src/App.tsx`):\n\n### 2. Metadata-Driven UI > 2.1 Metadata Fetcher\n\n**Progress**: 3/3 (100%)\n\n**Completed:**\n\n- \u2705 Create `useDocTypeMeta` hook:\n- \u2705 Fetch from `GET /api/meta/{doctype}`\n- \u2705 Cache metadata aggressively using React Query\n\n### 2. Metadata-Driven UI > 2.2 Auto Form Generator (RJSF Integration)\n\n**Progress**: 14/14 (100%)\n\n**Completed:**\n\n- \u2705 Create `AutoForm` component using RJSF:\n- \u2705 Accept JSON Schema from `GET /api/meta/{doctype}`\n- \u2705 Generate form fields dynamically\n- \u2705 Map schema types to shadcn/ui components:\n- \u2705 `string` \u2192 ``\n- \u2705 `number` \u2192 ``\n- \u2705 `boolean` \u2192 ``\n- \u2705 `enum` \u2192 `