# Architecture Cleanup Plan — LeoCRM **Erstellt:** 2026-08-14 **Basis:** Adversarial Architecture Audit (P0-1 bis P0-10, P1-9 bis P1-24) **Ziel:** Plugin-System funktionsfähig machen, Hartcodierungen entfernen, Core/Plugin-Grenze etablieren --- ## Prinzipien 1. **Jeder Fix nutzt existierende Interfaces** — kein Neubau, nur Verdrahtung 2. **Jeder Fix ist testbar** — Plugin aktivieren/deaktivieren ohne Neustart muss funktionieren 3. **Keine neuen Hartcodierungen** — jede neue Entität/Permission/Job kommt aus Plugin-Manifesten 4. **Minimal-invasiv** — Core-Routes bleiben statisch (nicht migrieren), nur Plugin-Teile dynamisieren 5. **Phase für Phase verifizierbar** — jede Phase hat klare Acceptance Criteria --- ## Phase 1: Plugin-Lifecycle zur Laufzeit funktionsfähig machen **Priorität:** P0-Kritisch | **Aufwand:** ~4h | **Abhängigkeiten:** keine ### Problem Plugin-Aktivierung/Deaktivierung zur Laufzeit funktioniert nicht (P0-10, P0-9, P0-1). ### Tasks #### 1.1 Permission-Registry bei Runtime-Aktivierung aktualisieren (P0-10) **Datei:** `app/services/plugin_service.py` **Änderung:** - In `activate_plugin()`: Nach `registry.activate()` → `register_plugin_permissions(name, plugin.manifest.permissions)` aufrufen - In `deactivate_plugin()`: Nach `registry.deactivate()` → `unregister_plugin_permissions(name)` aufrufen - `from app.core.permission_registry import register_plugin_permissions, unregister_plugin_permissions` **Verifikation:** 1. Plugin via API aktivieren → Permission-Check für Plugin-Route gibt kein 403 mehr 2. Plugin via API deaktivieren → Permission-Registry enthält Plugin nicht mehr 3. Test: `test_plugin_lifecycle.py` — aktivieren, Route callen, deaktivieren, Route gibt 403 #### 1.2 Route-Registrierung aus Discovery-Ergebnis (P0-1) **Datei:** `app/main.py:583-605` **Änderung:** - Ersetze hartcodierte `plugin_modules`-Liste mit `registry.list_discovered()` - Für jeden discovered Plugin: Lese Manifest-Routes, registriere mit `require_active_plugin()` - Behalte `try/except` für robustness ```python # ALT: plugin_modules = ["app.plugins.builtins.tags", ...] # NEU: registry = get_registry() for plugin_name in registry.list_discovered(): plugin = registry.get_plugin(plugin_name) if plugin and plugin.manifest.routes: for route_def in plugin.manifest.routes: # ... gleiche Logik wie bisher, aber dynamisch ``` **Verifikation:** 1. Neues Plugin-Verzeichnis in `app/plugins/builtins/` erstellen → Routes erscheinen ohne main.py-Änderung 2. Alle existierenden Plugin-Routes noch vorhanden (OpenAPI check) 3. `builtins/__init__.py`-Imports entfernen → Discovery findet Plugins trotzdem #### 1.3 `_mounted_routes` befüllen oder Route-Removal dokumentieren (P0-9) **Datei:** `app/plugins/registry.py:36,690-692` **Änderung (Option A — empfohlen):** - In `activate()`: Nach `app.include_router()` → `_mounted_routes[name].append(router)` - Das erfordert, dass `activate()` Zugriff auf `app` hat (bereits via `self._app`) - Route-Removal in `deactivate()` funktioniert dann tatsächlich **ODER Option B — einfacher:** - Entferne Route-Removal-Logik aus `deactivate()` - Dokumentiere: Routes bleiben registriert, `require_active_plugin()` ist die einzige Verteidigung - Das ist die aktuelle Realität — nur ehrlich dokumentiert **Verifikation:** - Option A: Plugin deaktivieren → Route gibt 404 (nicht 403) - Option B: Plugin deaktivieren → Route gibt 403 (dokumentiert) #### 1.4 `builtins/__init__.py`-Imports entfernen (P0-6) **Datei:** `app/plugins/builtins/__init__.py` **Änderung:** - Entferne alle 10 hartcodierten Plugin-Imports - `discover_builtins()` findet Plugins via `pkgutil` — die `__init__.py`-Imports sind redundant - Behalte nur den Docstring **Verifikation:** 1. App startet ohne Fehler 2. `registry.list_discovered()` enthält alle 21 Plugins 3. Alle Plugin-Routes registriert ### Acceptance Criteria Phase 1 - [ ] Plugin via API aktivieren → Routes funktionieren ohne Neustart - [ ] Plugin via API deaktivieren → Routes geben 403/404 - [ ] Neues Plugin in `builtins/` ablegen → Routes erscheinen ohne Core-Änderung - [ ] `builtins/__init__.py` hat keine Plugin-Imports mehr - [ ] Test: `test_plugin_lifecycle.py` existiert und ist grün --- ## Phase 2: Plugin-Selbstregistrierung statt Core-Hartcodierung **Priorität:** P0-Hoch | **Aufwand:** ~6h | **Abhängigkeiten:** Phase 1 ### Problem Core registriert Plugin-Entities, Hooks, Restore-Configs, Worker-Jobs hartkodiert (P0-7, P0-8, P0-5). ### Tasks #### 2.1 Restore-Registry: Plugins registrieren selbst (P0-7) **Dateien:** - `app/core/restore_registry.py:113-195` — entferne `register_default_entities()` für Plugin-Entities - `app/plugins/builtins/tasks/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="task", ...))` - `app/plugins/builtins/calendar/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="calendar_entry", ...))` - `app/plugins/builtins/dms/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="dms_file", ...))` - `app/plugins/builtins/mail/plugin.py` — in `on_activate()`: `reg.register(RestoreConfig(entity_type="mail", ..., special_handler=_mail_restore_handler))` - `app/plugins/builtins/mail/plugin.py` — `_mail_restore_handler` nach Mail-Plugin verschieben **Core behält nur:** Contact-Registrierung (Contact ist Core) **Verifikation:** 1. Plugin aktivieren → Restore für Plugin-Entity funktioniert 2. Plugin deaktivieren → Restore-Config für Plugin-Entity entfernt 3. `register_default_entities()` registriert nur noch Contact #### 2.2 History-Hooks: Plugins registrieren selbst (P0-8) **Dateien:** - `app/core/history_hooks.py:135-173` — entferne Plugin-Entity-Hooks aus `register_default_history_hooks()` - `app/plugins/builtins/tasks/plugin.py` — in `on_activate()`: `register_history_hooks(reg, "task", "task.after_create", ...)` - `app/plugins/builtins/calendar/plugin.py` — gleiche für `calendar_entry` - `app/plugins/builtins/dms/plugin.py` — gleiche für `dms_file` - `app/plugins/builtins/mail/plugin.py` — gleiche für `mail` **Core behält nur:** Contact-Hooks **Verifikation:** 1. Plugin aktivieren → History wird für Plugin-Entities aufgezeichnet 2. Plugin deaktivieren → Hooks werden entfernt (`on_deactivate` muss `reg.unregister_action()` aufrufen) 3. `register_default_history_hooks()` registriert nur noch Contact #### 2.3 Worker-Jobs: Plugins registrieren selbst (P0-5) **Dateien:** - `app/core/worker.py:221-227` — entferne hartcodierte `plugin_job_modules`-Liste - `app/plugins/base.py` — füge `get_job_modules() -> list[str]` hinzu (default: `[]`) - Jedes Plugin mit Jobs: überschreibe `get_job_modules()` → return `["app.plugins.builtins..jobs"]` - `worker.py` — iteriere `registry.list_discovered()`, rufe `plugin.get_job_modules()` auf, importiere dynamisch **Verifikation:** 1. Plugin mit Jobs aktivieren → Jobs laufen 2. Plugin deaktivieren → Jobs werden nicht mehr geladen 3. Neues Plugin mit Jobs → funktioniert ohne worker.py-Änderung #### 2.4 OpenAPI-Tags dynamisch aus Manifesten (P1-18) **Datei:** `app/main.py:300-370` **Änderung:** - Entferne Plugin-spezifische OpenAPI-Tags (`dms`, `mail`, `calendar`, `search`, etc.) - Behalte nur Core-Tags (`health`, `auth`, `users`, `contacts`, etc.) - Nach Plugin-Route-Registrierung: füge Tags aus Plugin-Manifest hinzu **Verifikation:** 1. OpenAPI-Schema enthält alle Plugin-Tags mit Beschreibungen 2. Plugin deaktivieren → Tag verschwindet aus OpenAPI ### Acceptance Criteria Phase 2 - [ ] `register_default_entities()` registriert nur Contact - [ ] `register_default_history_hooks()` registriert nur Contact - [ ] `worker.py` hat keine hartkodierte Plugin-Modulliste - [ ] `main.py` OpenAPI-Tags enthalten keine Plugin-Tags mehr - [ ] Plugin deaktivieren entfernt Restore-Config, History-Hooks, Worker-Jobs - [ ] Plugin aktivieren registriert alles neu --- ## Phase 3: Generische Services erweiterbar machen **Priorität:** P1-Hoch | **Aufwand:** ~8h | **Abhängigkeiten:** Phase 2 ### Problem ENTITY_MODELS, CORE_PERMISSIONS, saved_views, tags haben hartcodierte Entity-Types (P0-3, P0-4, P1-12, P1-13). ### Tasks #### 3.1 ENTITY_MODELS: Plugin-Registrierungs-Interface (P0-3) **Dateien:** - `app/services/entity_permission_service.py:70-190` — entferne alle `try/except` Plugin-Import-Blöcke - `app/plugins/base.py` — füge `get_entity_models() -> dict[str, type]` hinzu (default: `{}`) - Jedes Plugin: überschreibe `get_entity_models()` → return `{"file": DmsFile, "folder": DmsFolder}` - `entity_permission_service.py` — neue Funktion `register_entity_model(entity_type, model_class)` - `main.py:lifespan()` — nach Plugin-Aktivierung: iteriere Plugins, rufe `get_entity_models()`, registriere - `registry.activate()` — rufe `register_entity_model()` für aktive Plugins - `registry.deactivate()` — entferne Entity-Models für deaktivierte Plugins **Core behält:** Contact, Address, Attachment, BankAccount, Workflow, Sequence, SavedFilter, SavedView, Webhook, CustomFieldDefinition, ContactFolder, EntityAttachment, EntityHistory **Verifikation:** 1. Plugin aktivieren → ENTITY_MODELS enthält Plugin-Entities 2. Plugin deaktivieren → ENTITY_MODELS enthält Plugin-Entities nicht mehr 3. Permission-Resolution für Plugin-Entity funktioniert 4. Neues Plugin mit neuer Entität → funktioniert ohne entity_permission_service.py-Änderung #### 3.2 CORE_PERMISSIONS: Plugin-Permissions entfernen (P0-4) **Datei:** `app/core/permission_registry.py:21-128` **Änderung:** - Entferne alle Plugin-Permissions aus `CORE_PERMISSIONS` (calendar, dms, mail, tasks, comm, automation, ai, tags, entity_links, reports, search, mcp, permissions, agents, dashboard) - Behalte nur echte Core-Permissions: contacts, users, roles, groups, audit, settings, plugins, tenants, notifications, attachments, workflows, user_preferences, sequences, addresses, taxes, currencies, import_export, workspaces, system - Plugin-Permissions kommen bereits via `register_plugin_permissions()` aus Manifesten — das ist die dynamische Quelle - Entferne Kommentar "Plugin permissions (registered at startup, but also listed here for completeness)" **Verifikation:** 1. Plugin aktivieren → Plugin-Permissions in Registry 2. Plugin deaktivieren → Plugin-Permissions nicht in Registry 3. Permission-UI zeigt nur aktive Plugin-Permissions 4. Core-Permissions weiterhin verfügbar #### 3.3 CORE_FIELD_DEFINITIONS: Contact-Felder als Plugin oder Core-Deklaration (P1-16) **Datei:** `app/core/permission_registry.py:135-176` **Änderung:** - Contact-Felddefinitionen bleiben in Core (Contact ist Core) - User-Felddefinitionen bleiben in Core (User ist Core) - Das ist akzeptabel — Core darf Core-Felder deklarieren - **Kein Fix nötig** — nur Dokumentation dass dies Core-spezifisch ist #### 3.4 saved_views/saved_filters: Entity-Types dynamisch (P1-12) **Dateien:** `app/routes/saved_views.py:19`, `app/routes/saved_filters.py:19` **Änderung:** - Entferne `VALID_ENTITY_TYPES = {"contacts", "mail", "calendar", "dms"}` - Entferne Pydantic `pattern="^(contacts|mail|calendar|dms)$"` - Stattdessen: Validiere gegen `ENTITY_MODELS.keys()` oder eine neue `get_valid_entity_types()` Funktion - Akzeptiere jeden String, validiere zur Laufzeit gegen registrierte Entity-Types **Verifikation:** 1. Saved View für `task` erstellen → funktioniert 2. Saved View für `nonexistent` erstellen → 422 3. Plugin deaktivieren → Saved Views für Plugin-Entity noch abrufbar aber nicht neu erstellbar #### 3.5 tags/entity_links: VALID_ENTITY_TYPES dynamisch (P1-13) **Dateien:** `app/plugins/builtins/tags/routes.py:25`, `app/plugins/builtins/entity_links/routes.py:22` **Änderung:** - Entferne hartcodierte Sets - Tags: Validiere gegen `ENTITY_MODELS.keys()` (jede registrierte Entität kann getaggt werden) - Entity-Links: Validiere gegen `ENTITY_MODELS.keys()` (jede registrierte Entität kann verlinkt werden) - Frontend `tags.ts:12` — `EntityType` dynamisch aus API laden oder als `string` deklarieren **Verifikation:** 1. Tag für `task` erstellen → funktioniert 2. Tag für `nonexistent` → 422 3. Frontend zeigt alle verfügbaren Entity-Types an #### 3.6 Dashboard-Counts dynamisch (P1-21) **Datei:** `app/routes/dashboard.py:57-110` **Änderung:** - Behalte Contact/Company/Person als Core-Counts - Füge Plugin-Counts-Interface hinzu: `BasePlugin.get_dashboard_counts(db, tenant_id, user_id) -> list[dict]` - `/counts`-Endpoint iteriert aktive Plugins, sammelt Counts - Plugins können eigene Counts beitragen (z.B. Tasks: offene Tasks, Mail: ungelesene Mails) **Verifikation:** 1. Dashboard zeigt Plugin-Counts an 2. Plugin deaktivieren → Plugin-Counts verschwinden ### Acceptance Criteria Phase 3 - [ ] `ENTITY_MODELS` enthält keine `try/except` Plugin-Import-Blöcke mehr - [ ] `CORE_PERMISSIONS` enthält keine Plugin-Permissions mehr - [ ] saved_views/saved_filters akzeptieren alle registrierten Entity-Types - [ ] tags/entity_links akzeptieren alle registrierten Entity-Types - [ ] Dashboard-Counts enthalten Plugin-Beiträge - [ ] Plugin deaktivieren entfernt Permissions, Entity-Models aus Registries --- ## Phase 4: Core/Plugin-Abhängigkeiten reduzieren **Priorität:** P1-Mittel | **Aufwand:** ~6h | **Abhängigkeiten:** Phase 3 ### Problem Core-Dateien importieren direkt Plugin-Modelle (P1-9, P1-10, P1-19, P1-20). ### Tasks #### 4.1 Core→Plugin-Imports durch Contracts ersetzen (P1-9, P1-10) **Dateien (41 Core→Plugin-Imports):** - `app/core/notifications.py:43` → nutze `get_contract("kommunikation")` statt direktem Import - `app/core/restore_registry.py:132-237` → nach Phase 2.1 erledigt (Plugins registrieren selbst) - `app/core/trigger_dispatcher.py:116,174` → nutze `get_contract("automation")` - `app/core/worker.py:169,221-227,273` → nach Phase 2.3 erledigt (dynamische Job-Discovery) - `app/services/entity_permission_service.py:86-187` → nach Phase 3.1 erledigt (dynamische ENTITY_MODELS) - `app/services/attachment_service.py:25` → nutze `get_contract("dms")` für File-Modell - `app/commands/mail_commands.py:16,36,109,147` → nutze `get_contract("mail")` - `app/commands/calendar_commands.py:38,104,153` → nutze `get_contract("calendar")` - `app/commands/dms_commands.py:50,130` → nutze `get_contract("dms")` - `app/ai/llm_client.py:292,319` → nutze `get_contract("ai_assistant")` - `app/routes/errors.py:124` → nutze `get_contract("forgejo_error_reporter")` oder mache Error-Reporting generisch - `app/main.py:151,171` → gleiche wie errors.py **Verifikation:** 1. `grep -rn 'from app.plugins.builtins' app/core/ app/services/ app/routes/ app/commands/ app/ai/` → 0 Treffer (außer contracts) 2. Plugin deaktivieren → Core funktioniert ohne Fehler (graceful degradation) 3. `check_cross_plugin_imports.py` erweitert auf Core-Verzeichnisse #### 4.2 Cross-Plugin-Import-Checker auf Core erweitern (P1-20) **Datei:** `scripts/check_cross_plugin_imports.py` **Änderung:** - `find_python_files()` default search_path: auch `app/core/`, `app/services/`, `app/routes/`, `app/commands/`, `app/ai/` scannen - Neue EXEMPT_PATHS für legitime Core-Imports (z.B. `main.py` für Route-Registrierung) - CI-Pipeline prüft nun Core→Plugin-Imports auch **Verifikation:** 1. `python scripts/check_cross_plugin_imports.py` findet 0 Verstöße 2. CI-Pipeline grün #### 4.3 Cross-Plugin-Imports in Plugins auf Contracts umstellen (P1-19) **Aufwand:** Hoch (226 Imports), aber mechanisch **Priorisierung:** - Start mit Plugins, die am häufigsten importiert werden (kommunikation, ai_assistant, unified_search) - Jeder `from app.plugins.builtins.. import X` → `get_contract("")` mit None-Check - Contracts müssen alle aktuell direkt importierten Symbole exponieren **Verifikation:** 1. `grep -rn 'from app.plugins.builtins' app/plugins/builtins/ | grep -v contracts | grep -v __init__` → 0 2. Alle Plugin-Tests grün 3. Plugin deaktivieren → abhängige Plugins degradieren gracefully ### Acceptance Criteria Phase 4 - [ ] 0 Core→Plugin-Imports (außer contracts) - [ ] Cross-Plugin-Checker prüft Core-Verzeichnisse - [ ] Cross-Plugin-Imports in Plugins reduziert um >80% - [ ] Plugin deaktivieren → keine Import-Fehler in Core oder anderen Plugins --- ## Phase 5: Test-Infrastruktur und Qualität **Priorität:** P1-Mittel | **Aufwand:** ~4h | **Abhängigkeiten:** Phase 1-4 ### Problem Tests mocken Permissions weg, conftest importiert alle Plugins hartkodiert, keine E2E-Tests für Plugin-Lifecycle (P1-14, P1-15). ### Tasks #### 5.1 conftest.py: Plugin-Modelle dynamisch laden (P1-14) **Datei:** `tests/conftest.py:50-80` **Änderung:** - Entferne alle hartcodierten Plugin-Model-Imports - Stattdessen: iteriere `registry.list_discovered()`, rufe `plugin.get_entity_models()` auf, importiere Modelle dynamisch - `Base.metadata.create_all()` findet alle Tabellen weil Modelle importiert wurden ```python # ALT: 15 hartcodierte Imports from app.plugins.builtins.calendar.models import Calendar, CalendarEntry, ... # NEU: registry = get_registry() registry.discover_builtins() for name in registry.list_discovered(): plugin = registry.get_plugin(name) if plugin: models = plugin.get_entity_models() # Import module to register models with Base.metadata for entity_type, model_class in models.items(): # Model class is already imported via get_entity_models() pass ``` **Verifikation:** 1. Tests laufen ohne hartcodierte Plugin-Imports 2. Plugin entfernen → Tests für Plugin laufen nicht, aber andere Tests grün 3. Neues Plugin → Tests finden Modelle automatisch #### 5.2 Plugin-Lifecycle E2E-Test (neu) **Datei:** `tests/test_plugin_lifecycle.py` (neu) **Inhalt:** 1. Test-Plugin erstellen (minimal, mit Route, Permission, Entity-Model, Job) 2. Plugin aktivieren via API → Route erreichbar, Permission verfügbar, Job registriert 3. Plugin deaktivieren via API → Route gibt 403/404, Permission entfernt, Job entfernt 4. Plugin wieder aktivieren → alles wieder da 5. Plugin mit Dependency aktivieren → funktioniert nur wenn Dependency aktiv 6. Plugin mit Dependency deaktivieren → wird blockiert wenn Dependency aktiv **Verifikation:** 1. Test ist grün 2. Test läuft ohne Mocks für Permission/Visibility/Tenant #### 5.3 Permission-Mock-Tests umstellen (P1-15) **Dateien:** `tests/test_graph_rag.py:42`, `test_agent_memory.py:42`, `test_marketplace.py:48`, `test_external_agent_api.py:38` **Änderung:** - Entferne `patch("app.core.permissions.check_permission", return_value=True)` - Stattdessen: Test-User mit echten Permissions erstellen - `conftest.py` hat bereits `create_test_user` mit Role → nutze echte Permissions - Für `set_tenant_context` Mocks: nutze echte DB-Session mit Tenant-Kontext **Verifikation:** 1. Tests laufen ohne Permission-Mocks 2. Tests testen echte Permission-Enforcement 3. Test mit unzureichenden Permissions → 403 (nicht 200) ### Acceptance Criteria Phase 5 - [ ] `conftest.py` hat keine hartcodierten Plugin-Imports - [ ] `test_plugin_lifecycle.py` existiert und ist grün - [ ] Keine `patch("app.core.permissions.check_permission")` mehr in Tests - [ ] Plugin-Lifecycle E2E-Test testet echte Permission/Visibility/Tenant-Isolation --- ## Phase 6: Doppelarchitekturen auflösen **Priorität:** P1-Niedrig | **Aufwand:** ~4h | **Abhängigkeiten:** Phase 4 ### Problem Notification-Doppelarchitektur, Dedup/Import-Export Contact-spezifisch (P1-11, P1-22, P1-23). ### Tasks #### 6.1 Notification-Doppelarchitektur dokumentieren oder auflösen (P1-11) **Datei:** `app/core/notifications.py` **Änderung:** - `create_notification()` als deprecated markieren (bereits getan) - Frontend `NotificationDropdown` auf Communication-API umstellen - `app/routes/notifications.py` als deprecated markieren oder auf Communication redirect - Langfristig: `notifications`-Tabelle entfernen, alles über Communication-Plugin **Verifikation:** 1. Frontend nutzt Communication-API für Notifications 2. `notifications`-Route gibt Deprecation-Warning #### 6.2 Dedup-Service: Plugin-Interface oder als Contact-Service deklarieren (P1-22) **Datei:** `app/services/dedup_service.py` **Änderung (Option A — Plugin-Interface):** - `BasePlugin.get_dedup_config() -> DedupConfig | None` hinzufügen - Plugins deklarieren Dedup-Felder und Match-Logik - Dedup-Service iteriert aktive Plugins - **Aufwand:** Hoch — generische Dedup-Engine **ODER Option B — ehrlich deklarieren:** - `dedup_service.py` → `contact_dedup_service.py` umbenennen - Dokumentieren: Dedup ist Contact-spezifisch, nicht generisch - **Aufwand:** Klein — nur Umbenennung und Doku **Empfehlung:** Option B — Dedup ist CRM-spezifisch, muss nicht generisch sein. #### 6.3 Import/Export: Plugin-Interface oder als Contact-Service deklarieren (P1-23) **Datei:** `app/services/import_export_service.py` **Gleiche Entscheidung wie 6.2:** - Option A: Generisches Import/Export-Interface für Plugins - Option B: Als `contact_import_export_service.py` deklarieren **Empfehlung:** Option B für jetzt, Option A wenn ein Plugin Import/Export braucht. ### Acceptance Criteria Phase 6 - [ ] Notification-Doppelarchitektur aufgelöst oder dokumentiert - [ ] Dedup/Import-Export als Contact-spezifisch deklariert oder generisch gemacht --- ## Gesamtaufwand | Phase | Aufwand | Priorität | Abhängigkeit | |-------|---------|-----------|-------------| | 1: Plugin-Lifecycle | ~4h | P0-Kritisch | keine | | 2: Selbstregistrierung | ~6h | P0-Hoch | Phase 1 | | 3: Generische Services | ~8h | P1-Hoch | Phase 2 | | 4: Core/Plugin-Abhängigkeiten | ~6h | P1-Mittel | Phase 3 | | 5: Test-Infrastruktur | ~4h | P1-Mittel | Phase 1-4 | | 6: Doppelarchitekturen | ~4h | P1-Niedrig | Phase 4 | | **Total** | **~32h** | | | Bei 8h/Tag: **4 Arbeitstage** für alle Phasen. Phase 1 allein: **einen halben Tag**. --- ## Risiken 1. **Phase 1 kann versteckte Abhängigkeiten aufdecken** — wenn Plugin-Aktivierung zur Laufzeit zum ersten Mal richtig getestet wird, können neue Bugs sichtbar werden 2. **Phase 3.1 (ENTITY_MODELS)** ist der komplexeste Fix — das Permission-System hängt davon ab 3. **Phase 4.3 (226 Cross-Imports)** ist mechanisch aber fehleranfällig — jeder Contract muss alle Symbole exponieren 4. **Tests können brechen** — wenn Permission-Mocks entfernt werden, können Tests failen die vorher grün waren (was gut ist, aber Aufwand bedeutet) ## Erfolgsmessung Nach Abschluss aller Phasen: 1. **Plugin hinzufügen:** 0 Core-Dateien ändern → Plugin in `builtins/` ablegen, aktivieren 2. **Plugin deaktivieren:** Alle Routes, Permissions, Jobs, Hooks, Entity-Models entfernt 3. **Plugin entfernen:** `uninstall` → alle Spuren gelöscht 4. **Neue Entität:** Plugin deklariert Entität in Manifest → Permissions, Tags, Links, Saved Views funktionieren 5. **Cross-Plugin-Checker:** 0 Verstöße in Core und Plugins 6. **E2E-Test:** Plugin-Lifecycle-Test grün ohne Mocks Das ist das Ziel: **Ein Plugin-System, das wirklich modular ist.**