{
"tool_response": {
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/docs\/plan-form-components-refactoring.md",
"oldString": "## 10. Erfolgskriterien\n\n- [ ] Jedes Form-Element existiert nur 1x als Partial\n- [ ] Alle Views nutzen dieselben Partials\n- [ ] Einheitliche CSS-Klassen\n- [ ] Einheitliche Variablennamen\n- [ ] Collection-Labels überall deutsch\n- [ ] Modell-Dropdown überall mit Optgroups",
"newString": "## 10. Erfolgskriterien\n\n- [ ] Jedes Form-Element existiert nur 1x als Partial\n- [ ] Alle Views nutzen dieselben Partials\n- [ ] Einheitliche CSS-Klassen\n- [ ] Einheitliche Variablennamen\n- [ ] Collection-Labels überall deutsch\n- [ ] Modell-Dropdown überall mit Optgroups\n\n---\n\n## 11. Externe Supervision: Fragen und Antworten\n\n### 11.1 Grundannahmen und Systemgrenzen\n\n#### Q1.1: Single-Tenant vs. Multi-Tenant?\n\n**Status:** Single-Tenant (bewusste Entscheidung)\n\n| Aspekt | Entscheidung | Begründung |\n|--------|--------------|------------|\n| **Aktuell** | Single-Tenant | Ein Benutzer, ein System |\n| **user_id DEFAULT 1** | Zulässig | Placeholder für spätere Erweiterung |\n| **Migration zu Multi-Tenant** | Nicht geplant | YAGNI - kein absehbarer Business Case |\n\n**Risiko:** Falls Multi-Tenant später benötigt:\n- `user_id` ist bereits in Schema\n- Alle Queries müssten `WHERE user_id = ?` erhalten\n- Keine strukturelle Schuld, nur funktionale Erweiterung\n\n#### Q1.2: Laufzeitkritisch vs. Administrativ?\n\n**Fehlende Definition - ERGÄNZUNG ERFORDERLICH:**\n\n| Komponente | Klassifikation | Ausfallverhalten |\n|------------|----------------|------------------|\n| **Chat-Interface** | Laufzeitkritisch | Muss funktionieren |\n| **Content Studio** | Laufzeitkritisch | Muss funktionieren |\n| **Admin: \/ressourcen\/llm** | Administrativ | Kann temporär ausfallen |\n| **Admin: \/ressourcen\/collections** | Administrativ | Kann temporär ausfallen |\n| **Admin: \/ressourcen\/profil** | Administrativ | Fallback auf System-Defaults |\n| **Sync-Buttons** | Administrativ | Manueller Retry akzeptabel |\n\n**Empfehlung:** Diese Klassifikation in Operations-Dokumentation aufnehmen.\n\n#### Q1.3: Warum Qdrant Pflicht, Anthropic optional?\n\n| Dienst | Klassifikation | Begründung |\n|--------|----------------|------------|\n| **Qdrant** | Pflicht | RAG ist Kernfunktion, kein sinnvoller Fallback |\n| **Ollama** | Pflicht | Embeddings + lokale Modelle |\n| **Anthropic** | Optional | Fallback auf Ollama-Modelle möglich |\n\n**Klarstellung:** \"Optional\" bedeutet nicht \"unwichtig\", sondern \"degraded mode möglich\":\n\n```\nAnthropic down → Ollama-Modelle nutzbar (reduzierte Qualität)\nOllama down → System nicht funktional\nQdrant down → RAG nicht funktional, reiner Chat möglich\n```\n\n#### Q1.4: Fallback-Strategie (fehlt formal)\n\n**LÜCKE IDENTIFIZIERT - Neue Section erforderlich:**\n\n```php\n\/\/ Vorschlag: FallbackStrategy.php\nclass FallbackStrategy\n{\n public const STRATEGIES = [\n 'anthropic_unavailable' => [\n 'action' => 'use_ollama',\n 'notify' => true,\n 'log_level' => 'warning',\n ],\n 'ollama_unavailable' => [\n 'action' => 'fail_gracefully',\n 'notify' => true,\n 'log_level' => 'critical',\n ],\n 'qdrant_unavailable' => [\n 'action' => 'disable_rag',\n 'notify' => true,\n 'log_level' => 'error',\n ],\n ];\n}\n```\n\n---\n\n### 11.2 LLM-Verwaltung und Modellannahmen\n\n#### Q2.1: Quelle für context_window und max_output_tokens?\n\n| Provider | Quelle | Zuverlässigkeit |\n|----------|--------|-----------------|\n| **Anthropic** | API Response `\/v1\/models` | Autoritativ |\n| **Ollama** | `ollama show <model>` | Lokal korrekt |\n| **Manuell** | Admin-Eingabe | Fehleranfällig |\n\n**Risiko:** Cloud-Modelle ändern sich ohne Sync.\n\n**Mitigation (ERGÄNZUNG):**\n```sql\nALTER TABLE llm_models\nADD COLUMN params_verified_at DATETIME,\nADD COLUMN params_source ENUM('api', 'manual', 'estimated') DEFAULT 'estimated';\n```\n\n#### Q2.2: Veraltete Modellparameter?\n\n**Problem:** Token-Kalkulation basiert auf `max_output_tokens`.\n\n**Lösung:**\n1. **Sync-Warnung:** Wenn `last_synced_at > 7 days` → UI-Hinweis\n2. **Laufzeit-Check:** API-Fehler `max_tokens exceeded` → Parameter korrigieren\n3. **Konservative Defaults:** Bei Unsicherheit 75% von max_output_tokens\n\n```php\n\/\/ TokenCalculator.php\npublic function getSafeMaxTokens(array $model): int\n{\n $age = time() - strtotime($model['last_synced_at'] ?? '1970-01-01');\n $confidence = $age < 86400 ? 1.0 : ($age < 604800 ? 0.9 : 0.75);\n\n return (int) ($model['max_output_tokens'] * $confidence);\n}\n```\n\n#### Q2.3: Warum fehlende Modelle nicht deaktivieren?\n\n**Bewusste Entscheidung:**\n\n| Szenario | Verhalten | Begründung |\n|----------|-----------|------------|\n| API temporär down | Modell bleibt aktiv | Keine false negatives |\n| Modell deprecated | `last_synced_at` veraltet | Admin-Review erforderlich |\n| Modell entfernt | Bleibt in DB | Historische Referenzen erhalten |\n\n**Empfehlung:** Neues Feld `api_status`:\n```sql\nALTER TABLE llm_models\nADD COLUMN api_status ENUM('available', 'deprecated', 'removed', 'unknown') DEFAULT 'unknown';\n```\n\n#### Q2.4: model_id Eindeutigkeit?\n\n**Problem:** `claude-opus-4-5-20251101` vs. `anthropic:claude-opus-4-5-20251101`\n\n**Aktuelle Lösung:** UNIQUE KEY auf `(provider, model_id)` - ausreichend.\n\n**Langfristig:** Interne UUID nicht nötig, da provider+model_id stabil.\n\n#### Q2.5: Warum ENUM statt Lookup für provider?\n\n| Option | Pro | Contra |\n|--------|-----|--------|\n| ENUM | Schnell, typsicher | Schema-Änderung bei neuem Provider |\n| Lookup-Table | Flexibel | Overhead, JOIN erforderlich |\n\n**Entscheidung:** ENUM bleibt. Neue Provider = seltenes Event, Schema-Migration akzeptabel.\n\n#### Q2.6: Ollama Tag-Wechsel (latest)?\n\n**Problem:** `mistral:latest` kann morgen anderes Modell sein.\n\n**Lösung:**\n```php\n\/\/ OllamaSyncService.php\npublic function syncModels(): void\n{\n $models = $this->ollama->list();\n\n foreach ($models as $model) {\n \/\/ Vollständigen Hash speichern\n $this->repository->upsert([\n 'model_id' => $model['name'], \/\/ \"mistral:latest\"\n 'model_digest' => $model['digest'], \/\/ \"abc123...\"\n 'display_name' => $model['name'],\n ]);\n }\n}\n```\n\n**ERGÄNZUNG im Schema:**\n```sql\nALTER TABLE llm_models\nADD COLUMN model_digest VARCHAR(64) COMMENT 'SHA256 bei Ollama';\n```\n\n---\n\n### 11.3 Sync-Logik und Ausfallsicherheit\n\n#### Q3.1: Anthropic Sync Fehler?\n\n**Fehlerbehandlung (ERGÄNZUNG):**\n\n```php\npublic function syncAnthropic(): SyncResult\n{\n try {\n $models = $this->anthropicClient->listModels();\n $this->processModels($models);\n\n return SyncResult::success(count($models));\n } catch (ApiException $e) {\n $this->logger->error('Anthropic sync failed', ['error' => $e->getMessage()]);\n\n return SyncResult::failed($e->getMessage());\n }\n}\n\n\/\/ UI zeigt:\n\/\/ ✓ Sync erfolgreich: 5 Modelle aktualisiert\n\/\/ ✗ Sync fehlgeschlagen: API nicht erreichbar (letzte erfolgreiche Sync: vor 2 Tagen)\n```\n\n#### Q3.2: Logging von Metadaten-Änderungen?\n\n**LÜCKE - Audit-Trail erforderlich:**\n\n```sql\nCREATE TABLE llm_model_history (\n id INT AUTO_INCREMENT PRIMARY KEY,\n model_id INT NOT NULL,\n field_name VARCHAR(50) NOT NULL,\n old_value TEXT,\n new_value TEXT,\n changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n changed_by VARCHAR(50) DEFAULT 'sync',\n\n INDEX idx_model (model_id),\n INDEX idx_changed (changed_at)\n);\n```\n\n#### Q3.3: shell_exec('ollama list') sicher?\n\n**Status:** Ja, unter Bedingungen.\n\n| Prüfung | Status |\n|---------|--------|\n| Ollama installiert | ✓ Pflichtkomponente |\n| Keine User-Eingabe in Command | ✓ Hardcoded |\n| Output-Parsing sicher | ✓ Kein eval() |\n| Timeout | ⚠️ FEHLT |\n\n**ERGÄNZUNG:**\n```php\n\/\/ Statt shell_exec():\n$process = proc_open(['ollama', 'list'], $descriptors, $pipes);\nstream_set_timeout($pipes[1], 10); \/\/ 10s timeout\n```\n\n#### Q3.4: Sync vs. laufende Sessions?\n\n**Problem:** Session nutzt `model_id`, Sync ändert Metadaten.\n\n**Analyse:**\n- `model_id` selbst ändert sich nicht\n- `max_output_tokens` Änderung → nächster Request nutzt neue Werte\n- **Kein Konsistenzproblem**, da Session-Settings separat gespeichert\n\n---\n\n### 11.4 Dropdown-Partials und UI-Semantik\n\n#### Q4.1: Warum feste Optgroups?\n\n**Pragmatische Entscheidung:**\n\n```php\n\/\/ Aktuell:\n<optgroup label=\"Cloud\">\n<optgroup label=\"Lokal\">\n\n\/\/ Datengetrieben wäre:\n<optgroup label=\"<?= $providerLabels[$provider] ?>\">\n```\n\n**Entscheidung:** Hardcoded bleibt, weil:\n1. Nur 2 relevante Kategorien\n2. Sortierung Cloud→Lokal ist gewollt\n3. Kein Business Case für dynamische Gruppen\n\n#### Q4.2: Dritter Provider-Typ?\n\n**Bei Bedarf erweitern:**\n```php\nconst PROVIDER_GROUPS = [\n 'cloud' => ['anthropic', 'openai', 'google'],\n 'local' => ['ollama'],\n 'private' => ['custom'], \/\/ Zukünftig\n];\n```\n\n#### Q4.3: Leeres $models?\n\n**Fehlerbehandlung (ERGÄNZUNG):**\n```php\n\/\/ model-select.php\n<?php if (empty($models)): ?>\n <select disabled class=\"form-select form-select--error\">\n <option>Keine Modelle verfügbar - Sync erforderlich<\/option>\n <\/select>\n<?php else: ?>\n \/\/ Normaler Select\n<?php endif; ?>\n```\n\n#### Q4.4: Warum id=\"model\" hardcoded?\n\n**Problem:** Mehrere Instanzen auf einer Seite → ID-Konflikt.\n\n**KORREKTUR:**\n```php\n$id = $id ?? 'model_' . uniqid();\n```\n\n---\n\n### 11.5 Collection-Verwaltung und RAG-Konsistenz\n\n#### Q5.1: Verantwortung für vector_size?\n\n**Verantwortlichkeiten:**\n\n| Aktion | Verantwortlich | Prüfung |\n|--------|----------------|---------|\n| Collection erstellen | Pipeline\/Admin | Muss Embedding-Modell angeben |\n| Embedding-Modell wechseln | Admin | Re-Embedding erforderlich |\n| vector_size validieren | Sync | Qdrant ist Source of Truth |\n\n#### Q5.2: Inkompatible Embeddings bei Multi-Select?\n\n**KRITISCHE LÜCKE:**\n\n```php\n\/\/ CollectionValidator.php (NEU)\npublic function validateSelection(array $collectionIds): ValidationResult\n{\n $collections = $this->repository->findByIds($collectionIds);\n $vectorSizes = array_unique(array_column($collections, 'vector_size'));\n\n if (count($vectorSizes) > 1) {\n return ValidationResult::error(\n 'Inkompatible Collections: Unterschiedliche Vektorgrößen'\n );\n }\n\n return ValidationResult::ok();\n}\n```\n\n#### Q5.3: embedding_model nur dokumentativ?\n\n**Aktuell:** Ja, rein informativ.\n\n**ERGÄNZUNG:** Validierung bei Suche:\n```php\n\/\/ Q... [TRUNCATED-2147dee6fdfc0f68]",
"originalFile": "# Plan: Einheitliche Form-Komponenten (Chat & Content Studio)\n\n## Status: PLANUNG\n**Erstellt:** 2025-12-21\n**Ziel:** DRY, KISS, SRP - Shared Partials für alle wiederverwendbaren Form-Elemente\n\n---\n\n## Projekt-Kontext (für externe Supervision)\n\n### Was ist das System?\n\n**Campus.systemische-tools.de** ist ein KI-gestütztes Content-Management-System mit zwei Hauptmodulen:\n\n| Modul | Beschreibung | Hauptfunktionen |\n|-------|--------------|-----------------|\n| **Chat-Interface** | RAG-basierte Konversation mit verschiedenen LLMs | Session-Management, Multi-Collection-Suche, Streaming-Responses |\n| **Content Studio** | Automatisierte Content-Generierung mit Qualitätssicherung | Briefing → Generierung → Kritik → Revision → Freigabe |\n\n### Warum dieses Refactoring?\n\n**Problem:**\n- Chat und Content Studio wurden unabhängig entwickelt\n- Gleiche UI-Elemente (Modell-Dropdown, Collections, etc.) existieren in unterschiedlichen Implementierungen\n- Inkonsistente UX: Modell-Dropdown mit\/ohne Optgroups, Collections als Multi-Select\/Checkboxes\/Single-Select\n\n**Symptome:**\n- Doppelter Code in Views\n- Unterschiedliche Variablennamen (`$profiles` vs `$authorProfiles`)\n- Keine zentrale Verwaltung für LLM-Modelle und Collections\n- Keine persistenten User-Präferenzen\n\n**Ziel:**\n- Einheitliche Partials für alle Form-Elemente\n- Zentrale Admin-Seiten für LLM-Modelle und Collections\n- User-Präferenzen mit 3-stufiger Werte-Hierarchie\n- Contracts und Structures in beiden Systemen nutzbar (Multi-Select)\n\n### Technologie-Stack\n\n| Schicht | Technologie | Details |\n|---------|-------------|---------|\n| **Backend** | PHP 8.2 | MVC-Pattern in `\/src\/` |\n| **Frontend** | HTML + HTMX | Reactive ohne SPA-Framework |\n| **Datenbank** | MariaDB 10.11 | Zwei Datenbanken (siehe unten) |\n| **Vector-DB** | Qdrant | RAG-Embeddings, Collections |\n| **LLM-APIs** | Anthropic Claude, Ollama | Cloud + lokal |\n| **Pipeline** | Python 3.11 | Content-Generierung, Embedding |\n| **Styling** | CSS Custom Properties | Theme-Support |\n\n### Datenbank-Architektur\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ MariaDB │\n├─────────────────────────────┬───────────────────────────────────┤\n│ ki_dev │ ki_content │\n│ (Infrastruktur\/Config) │ (Laufzeitdaten) │\n├─────────────────────────────┼───────────────────────────────────┤\n│ • llm_models [NEU] │ • chat_sessions [ERWEITERT] │\n│ • llm_presets [NEU] │ • chat_messages │\n│ • rag_collections [NEU] │ • content_orders [MIGRIERT] │\n│ • user_preferences [NEU] │ • content_versions │\n│ • content_config │ • content_critiques │\n│ • tasks │ • content_sources │\n│ • contracts │ │\n│ • dokumentation │ │\n└─────────────────────────────┴───────────────────────────────────┘\n```\n\n### Externe Abhängigkeiten\n\n| Dienst | Verwendung | Ausfallverhalten |\n|--------|------------|------------------|\n| **Anthropic API** | Claude-Modelle, Chat-Completion | Fallback auf Ollama |\n| **Ollama (lokal)** | Lokale Modelle, Embeddings | Pflichtkomponente |\n| **Qdrant (lokal)** | Vektor-Suche, Collection-Metadaten | Pflichtkomponente |\n\n### Architektur-Prinzipien (angewendet)\n\n| Prinzip | Anwendung in diesem Plan |\n|---------|--------------------------|\n| **DRY** | Partials als Single Source für Form-Elemente |\n| **SRP** | Jedes Partial = eine Aufgabe |\n| **KISS** | Context Limit hardcoded, keine Over-Engineering |\n| **YAGNI** | Section 8 definiert klare Grenzen |\n| **MVC** | Controller → View (Partials) → Repository |\n\n### Wichtige Konventionen\n\n1. **MCP-Server:** Alle DB-Zugriffe via MCP-Tools (kein direktes SQL)\n2. **Kein Git:** Server ohne Versionskontrolle\n3. **Dev-First:** Entwicklung auf dev.campus, dann Sync zu prod\n4. **Quality-Gate:** `\/opt\/scripts\/php-check.sh` muss vor Sync bestehen\n\n---\n\n## 0. LLM-Verwaltung (Grundlage für Modell-Dropdown)\n\n### 0.1 Architektur-Entscheidung\n\n**Gewählt: Variante B - Dedizierte LLM-Verwaltung**\n\nStatt dynamischer API-Abfrage bei jedem Seitenaufruf:\n- Zentrale Admin-Seite `\/ressourcen\/llm`\n- Datenbank-Tabelle als Single Source of Truth\n- Sync-Buttons für Provider (Anthropic, Ollama, weitere)\n- Sprechende Namen und Zusatzmetadaten\n\n### 0.2 Datenbank-Tabelle `llm_models`\n\n```sql\nCREATE TABLE llm_models (\n id INT AUTO_INCREMENT PRIMARY KEY,\n provider ENUM('anthropic', 'ollama', 'openai', 'google', 'mistral', 'custom') NOT NULL,\n model_id VARCHAR(100) NOT NULL, -- API-ID: \"claude-opus-4-5-20251101\"\n display_name VARCHAR(100) NOT NULL, -- Anzeige: \"Claude Opus 4.5\"\n description TEXT, -- Kurzbeschreibung\n context_window INT, -- Max. Tokens Input: 200000\n max_output_tokens INT, -- Max. Tokens Output: 8192\n input_price_per_mtok DECIMAL(10,4), -- Preis Input $\/MTok\n output_price_per_mtok DECIMAL(10,4), -- Preis Output $\/MTok\n capabilities JSON, -- {\"vision\": true, \"function_calling\": true}\n is_local BOOLEAN DEFAULT FALSE, -- Lokal (Ollama) vs. Cloud\n is_active BOOLEAN DEFAULT TRUE, -- In Dropdowns anzeigen?\n sort_order INT DEFAULT 0, -- Reihenfolge in Dropdowns\n last_synced_at DATETIME, -- Letzte Synchronisation\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n UNIQUE KEY unique_provider_model (provider, model_id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n```\n\n### 0.3 Admin-Seite `\/ressourcen\/llm`\n\n**URL:** `\/ressourcen\/llm`\n**Controller:** `RessourcenController::llmIndex()`\n\n**Funktionen:**\n\n| Button | Aktion |\n|--------|--------|\n| \"Anthropic synchronisieren\" | `GET api.anthropic.com\/v1\/models` → DB aktualisieren |\n| \"Ollama synchronisieren\" | `ollama list` → DB aktualisieren |\n| \"Neuer Provider\" | Manuell weiteren Provider hinzufügen |\n\n**Tabellen-Ansicht:**\n\n| Provider | Model-ID | Display-Name | Context | Preis In\/Out | Lokal | Aktiv | Aktionen |\n|----------|----------|--------------|---------|--------------|-------|-------|----------|\n| Anthropic | claude-opus-4-5-20251101 | Claude Opus 4.5 | 200k | $15\/$75 | - | ✓ | Bearbeiten |\n| Ollama | mistral:latest | Mistral 7B | 32k | - | ✓ | ✓ | Bearbeiten |\n\n**Bearbeiten-Dialog:**\n- Display-Name ändern\n- Beschreibung hinzufügen\n- Context-Window \/ Max-Output korrigieren\n- Preise eintragen (für Kostenberechnung)\n- Aktivieren\/Deaktivieren\n- Sortierung ändern\n\n### 0.4 Sync-Logik\n\n**Anthropic Sync:**\n```php\n\/\/ GET https:\/\/api.anthropic.com\/v1\/models\n\/\/ Response: {\"data\": [{\"id\": \"claude-opus-4-5-20251101\", ...}]}\n\nforeach ($apiModels as $model) {\n \/\/ INSERT ... ON DUPLICATE KEY UPDATE\n \/\/ Neue Modelle: is_active = true, display_name = model_id (initial)\n \/\/ Existierende: last_synced_at aktualisieren\n \/\/ Fehlende: NICHT löschen, nur last_synced_at bleibt alt\n}\n```\n\n**Ollama Sync:**\n```php\n\/\/ $ ollama list\n\/\/ NAME ID SIZE MODIFIED\n\/\/ mistral:latest abc123... 4.1 GB 2 days ago\n\n$output = shell_exec('ollama list');\n\/\/ Parsen und in DB einfügen\n```\n\n### 0.5 Dropdown-Query\n\n```php\n\/\/ ModelService::getActiveModels()\npublic function getActiveModels(): array\n{\n return $this->db->query(\"\n SELECT model_id, display_name, provider, is_local, context_window\n FROM llm_models\n WHERE is_active = 1\n ORDER BY is_local ASC, sort_order ASC, display_name ASC\n \")->fetchAll();\n}\n```\n\n### 0.6 Partial nutzt DB-Daten\n\n```php\n\/\/ \/src\/View\/partials\/form\/model-select.php\n<?php\n$models = $models ?? [];\n$selected = $selected ?? '';\n$variant = $variant ?? 'default';\n$class = $variant === 'inline' ? 'form-select--inline' : 'form-select';\n?>\n<select name=\"model\" id=\"model\" class=\"<?= $class ?>\">\n <optgroup label=\"Cloud\">\n <?php foreach ($models as $m): ?>\n <?php if (!$m['is_local']): ?>\n <option value=\"<?= $m['model_id'] ?>\" <?= $selected === $m['model_id'] ? 'selected' : '' ?>>\n <?= htmlspecialchars($m['display_name']) ?>\n <\/option>\n <?php endif; ?>\n <?php endforeach; ?>\n <\/optgroup>\n <optgroup label=\"Lokal\">\n <?php foreach ($models as $m): ?>\n <?php if ($m['is_local']): ?>\n <option value=\"<?= $m['model_id'] ?>\" <?= $selected === $m['model_id'] ? 'selected' : '' ?>>\n <?= htmlspecialchars($m['display_name']) ?>\n <\/option>\n <?php endif; ?>\n <?php endforeach; ?>\n <\/optgroup>\n<\/select>\n```\n\n### 0.7 Migrations-Schritte (LLM-Verwaltung)\n\n1. [ ] Tabelle `llm_models` in `ki_dev` erstellen\n2. [ ] `LlmModelRepository` erstellen\n3. [ ] `LlmSyncService` erstellen (Anthropic + Ollama)\n4. [ ] Route `\/ressourcen\/llm` anlegen\n5. [ ] `RessourcenController::llmIndex()` implementieren\n6. [ ] View `\/ressourcen\/llm\/index.php` erstellen\n7. [ ] Sync-Buttons implementieren\n8. [ ] Bearbeiten-Funktionalität\n9. [ ] Initial-Sync durchführen (bestehende Modelle importieren)\n10. [ ] `ModelConfig.php` durch `LlmModelRepository` ersetzen\n11. [ ] Partial `model-select.php` erstellen\n12. [ ] Chat und Content auf Partial umstellen\n\n---\n\n## 0b. Collection-Verwaltung (Grundlage für Collection-Dropdown)\n\n### 0b.1 Architektur-Entscheidung\n\n**Gewählt: DB-Tabelle analog zu LLMs**\n\n- Zentrale Admin-Seite `\/ressourcen\/collections`\n- Datenbank-Tabelle als Single Source of Truth\n- Sync mit Qdrant (Metadaten abrufen)\n- Sprechende Namen und Beschreibungen\n- **Multi-Select** als einheitliche Darstellung\n\n### 0b.2 Datenbank-Tabelle `rag_collections`\n\n```sql\nCREATE TABLE rag_collections (\n id INT AUTO_INCREMENT PRIMARY KEY,\n collection_id VARCHAR(100) NOT NULL UNIQUE, -- Qdrant-Name: \"documents\"\n display_name VARCHAR(100) NOT NULL, -- ... [TRUNCATED-e33c117a2bed6b0d]",
"structuredPatch": [
{
"oldStart": 2817,
"oldLines": 3,
"newStart": 2817,
"newLines": 837,
"lines": [
" - [ ] Einheitliche Variablennamen",
" - [ ] Collection-Labels überall deutsch",
" - [ ] Modell-Dropdown überall mit Optgroups",
"+",
"+---",
"+",
"+## 11. Externe Supervision: Fragen und Antworten",
"+",
"+### 11.1 Grundannahmen und Systemgrenzen",
"+",
"+#### Q1.1: Single-Tenant vs. Multi-Tenant?",
"+",
"+**Status:** Single-Tenant (bewusste Entscheidung)",
"+",
"+| Aspekt | Entscheidung | Begründung |",
"+|--------|--------------|------------|",
"+| **Aktuell** | Single-Tenant | Ein Benutzer, ein System |",
"+| **user_id DEFAULT 1** | Zulässig | Placeholder für spätere Erweiterung |",
"+| **Migration zu Multi-Tenant** | Nicht geplant | YAGNI - kein absehbarer Business Case |",
"+",
"+**Risiko:** Falls Multi-Tenant später benötigt:",
"+- `user_id` ist bereits in Schema",
"+- Alle Queries müssten `WHERE user_id = ?` erhalten",
"+- Keine strukturelle Schuld, nur funktionale Erweiterung",
"+",
"+#### Q1.2: Laufzeitkritisch vs. Administrativ?",
"+",
"+**Fehlende Definition - ERGÄNZUNG ERFORDERLICH:**",
"+",
"+| Komponente | Klassifikation | Ausfallverhalten |",
"+|------------|----------------|------------------|",
"+| **Chat-Interface** | Laufzeitkritisch | Muss funktionieren |",
"+| **Content Studio** | Laufzeitkritisch | Muss funktionieren |",
"+| **Admin: \/ressourcen\/llm** | Administrativ | Kann temporär ausfallen |",
"+| **Admin: \/ressourcen\/collections** | Administrativ | Kann temporär ausfallen |",
"+| **Admin: \/ressourcen\/profil** | Administrativ | Fallback auf System-Defaults |",
"+| **Sync-Buttons** | Administrativ | Manueller Retry akzeptabel |",
"+",
"+**Empfehlung:** Diese Klassifikation in Operations-Dokumentation aufnehmen.",
"+",
"+#### Q1.3: Warum Qdrant Pflicht, Anthropic optional?",
"+",
"+| Dienst | Klassifikation | Begründung |",
"+|--------|----------------|------------|",
"+| **Qdrant** | Pflicht | RAG ist Kernfunktion, kein sinnvoller Fallback |",
"+| **Ollama** | Pflicht | Embeddings + lokale Modelle |",
"+| **Anthropic** | Optional | Fallback auf Ollama-Modelle möglich |",
"+",
"+**Klarstellung:** \"Optional\" bedeutet nicht \"unwichtig\", sondern \"degraded mode möglich\":",
"+",
"+```",
"+Anthropic down → Ollama-Modelle nutzbar (reduzierte Qualität)",
"+Ollama down → System nicht funktional",
"+Qdrant down → RAG nicht funktional, reiner Chat möglich",
"+```",
"+",
"+#### Q1.4: Fallback-Strategie (fehlt formal)",
"+",
"+**LÜCKE IDENTIFIZIERT - Neue Section erforderlich:**",
"+",
"+```php",
"+\/\/ Vorschlag: FallbackStrategy.php",
"+class FallbackStrategy",
"+{",
"+ public const STRATEGIES = [",
"+ 'anthropic_unavailable' => [",
"+ 'action' => 'use_ollama',",
"+ 'notify' => true,",
"+ 'log_level' => 'warning',",
"+ ],",
"+ 'ollama_unavailable' => [",
"+ 'action' => 'fail_gracefully',",
"+ 'notify' => true,",
"+ 'log_level' => 'critical',",
"+ ],",
"+ 'qdrant_unavailable' => [",
"+ 'action' => 'disable_rag',",
"+ 'notify' => true,",
"+ 'log_level' => 'error',",
"+ ],",
"+ ];",
"+}",
"+```",
"+",
"+---",
"+",
"+### 11.2 LLM-Verwaltung und Modellannahmen",
"+",
"+#### Q2.1: Quelle für context_window und max_output_tokens?",
"+",
"+| Provider | Quelle | Zuverlässigkeit |",
"+|----------|--------|-----------------|",
"+| **Anthropic** | API Response `\/v1\/models` | Autoritativ |",
"+| **Ollama** | `ollama show <model>` | Lokal korrekt |",
"+| **Manuell** | Admin-Eingabe | Fehleranfällig |",
"+",
"+**Risiko:** Cloud-Modelle ändern sich ohne Sync.",
"+",
"+**Mitigation (ERGÄNZUNG):**",
"+```sql",
"+ALTER TABLE llm_models",
"+ADD COLUMN params_verified_at DATETIME,",
"+ADD COLUMN params_source ENUM('api', 'manual', 'estimated') DEFAULT 'estimated';",
"+```",
"+",
"+#### Q2.2: Veraltete Modellparameter?",
"+",
"+**Problem:** Token-Kalkulation basiert auf `max_output_tokens`.",
"+",
"+**Lösung:**",
"+1. **Sync-Warnung:** Wenn `last_synced_at > 7 days` → UI-Hinweis",
"+2. **Laufzeit-Check:** API-Fehler `max_tokens exceeded` → Parameter korrigieren",
"+3. **Konservative Defaults:** Bei Unsicherheit 75% von max_output_tokens",
"+",
"+```php",
"+\/\/ TokenCalculator.php",
"+public function getSafeMaxTokens(array $model): int",
"+{",
"+ $age = time() - strtotime($model['last_synced_at'] ?? '1970-01-01');",
"+ $confidence = $age < 86400 ? 1.0 : ($age < 604800 ? 0.9 : 0.75);",
"+",
"+ return (int) ($model['max_output_tokens'] * $confidence);",
"+}",
"+```",
"+",
"+#### Q2.3: Warum fehlende Modelle nicht deaktivieren?",
"+",
"+**Bewusste Entscheidung:**",
"+",
"+| Szenario | Verhalten | Begründung |",
"+|----------|-----------|------------|",
"+| API temporär down | Modell bleibt aktiv | Keine false negatives |",
"+| Modell deprecated | `last_synced_at` veraltet | Admin-Review erforderlich |",
"+| Modell entfernt | Bleibt in DB | Historische Referenzen erhalten |",
"+",
"+**Empfehlung:** Neues Feld `api_status`:",
"+```sql",
"+ALTER TABLE llm_models",
"+ADD COLUMN api_status ENUM('available', 'deprecated', 'removed', 'unknown') DEFAULT 'unknown';",
"+```",
"+",
"+#### Q2.4: model_id Eindeutigkeit?",
"+",
"+**Problem:** `claude-opus-4-5-20251101` vs. `anthropic:claude-opus-4-5-20251101`",
"+",
"+**Aktuelle Lösung:** UNIQUE KEY auf `(provider, model_id)` - ausreichend.",
"+",
"+**Langfristig:** Interne UUID nicht nötig, da provider+model_id stabil.",
"+",
"+#### Q2.5: Warum ENUM statt Lookup für provider?",
"+",
"+| Option | Pro | Contra |",
"+|--------|-----|--------|",
"+| ENUM | Schnell, typsicher | Schema-Änderung bei neuem Provider |",
"+| Lookup-Table | Flexibel | Overhead, JOIN erforderlich |",
"+",
"+**Entscheidung:** ENUM bleibt. Neue Provider = seltenes Event, Schema-Migration akzeptabel.",
"+",
"+#### Q2.6: Ollama Tag-Wechsel (latest)?",
"+",
"+**Problem:** `mistral:latest` kann morgen anderes Modell sein.",
"+",
"+**Lösung:**",
"+```php",
"+\/\/ OllamaSyncService.php",
"+public function syncModels(): void",
"+{",
"+ $models = $this->ollama->list();",
"+",
"+ foreach ($models as $model) {",
"+ \/\/ Vollständigen Hash speichern",
"+ $this->repository->upsert([",
"+ 'model_id' => $model['name'], \/\/ \"mistral:latest\"",
"+ 'model_digest' => $model['digest'], \/\/ \"abc123...\"",
"+ 'display_name' => $model['name'],",
"+ ]);",
"+ }",
"+}",
"+```",
"+",
"+**ERGÄNZUNG im Schema:**",
"+```sql",
"+ALTER TABLE llm_models",
"+ADD COLUMN model_digest VARCHAR(64) COMMENT 'SHA256 bei Ollama';",
"+```",
"+",
"+---",
"+",
"+### 11.3 Sync-Logik und Ausfallsicherheit",
"+",
"+#### Q3.1: Anthropic Sync Fehler?",
"+",
"+**Fehlerbehandlung (ERGÄNZUNG):**",
"+",
"+```php",
"+public function syncAnthropic(): SyncResult",
"+{",
"+ try {",
"+ $models = $this->anthropicClient->listModels();",
"+ $this->processModels($models);",
"+",
"+ return SyncResult::success(count($models));",
"+ } catch (ApiException $e) {",
"+ $this->logger->error('Anthropic sync failed', ['error' => $e->getMessage()]);",
"+",
"+ return SyncResult::failed($e->getMessage());",
"+ }",
"+}",
"+",
"+\/\/ UI zeigt:",
"+\/\/ ✓ Sync erfolgreich: 5 Modelle aktualisiert",
"+\/\/ ✗ Sync fehlgeschlagen: API nicht erreichbar (letzte erfolgreiche Sync: vor 2 Tagen)",
"+```",
"+",
"+#### Q3.2: Logging von Metadaten-Änderungen?",
"+",
"+**LÜCKE - Audit-Trail erforderlich:**",
"+",
"+```sql",
"+CREATE TABLE llm_model_history (",
"+ id INT AUTO_INCREMENT PRIMARY KEY,",
"+ model_id INT NOT NULL,",
"+ field_name VARCHAR(50) NOT NULL,",
"+ old_value TEXT,",
"+ new_value TEXT,",
"+ changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,",
"+ changed_by VARCHAR(50) DEFAULT 'sync',",
"+",
"+ INDEX idx_model (model_id),",
"+ INDEX idx_changed (changed_at)",
"+);",
"+```",
"+",
"+#### Q3.3: shell_exec('ollama list') sicher?",
"+",
"+**Status:** Ja, unter Bedingungen.",
"+",
"+| Prüfung | Status |",
"+|---------|--------|",
"+| Ollama installiert | ✓ Pflichtkomponente |",
"+| Keine User-Eingabe in Command | ✓ Hardcoded |",
"+| Output-Parsing sicher | ✓ Kein eval() |",
"+| Timeout | ⚠️ FEHLT |",
"+",
"+**ERGÄNZUNG:**",
"+```php",
"+\/\/ Statt shell_exec():",
"+$process = proc_open(['ollama', 'list'], $descriptors, $pipes);",
"+stream_set_timeout($pipes[1], 10); \/\/ 10s timeout",
"+```",
"+",
"+#### Q3.4: Sync vs. laufende Sessions?",
"+",
"+**Problem:** Session nutzt `model_id`, Sync ändert Metadaten.",
"+",
"+**Analyse:**",
"+- `model_id` selbst ändert sich nicht",
"+- `max_output_tokens` Änderung → nächster Request nutzt neue Werte",
"+- **Kein Konsistenzproblem**, da Session-Settings separat gespeichert",
"+",
"+---",
"+",
"+### 11.4 Dropdown-Partials und UI-Semantik",
"+",
"+#### Q4.1: Warum feste Optgroups?",
"+",
"+**Pragmatische Entscheidung:**",
"+",
"+```php",
"+\/\/ Aktuell:",
"+<optgroup label=\"Cloud\">",
"+<optgroup label=\"Lokal\">",
"+",
"+\/\/ Datengetrieben wäre:",
"+<optgroup label=\"<?= $providerLabels[$provider] ?>\">",
"+```",
"+",
"+**Entscheidung:** Hardcoded bleibt, weil:",
"+1. Nur 2 relevante Kategorien",
"+2. Sortierung Cloud→Lokal ist gewollt",
"+3. Kein Business Case für dynamische Gruppen",
"+",
"+#### Q4.2: Dritter Provider-Typ?",
"+",
"+**Bei Bedarf erweitern:**",
"+```php",
"+const PROVIDER_GROUPS = [",
"+ 'cloud' => ['anthropic', 'openai', 'google'],",
"+ 'local' => ['ollama'],",
"+ 'private' => ['custom'], \/\/ Zukünftig",
"+];",
"+```",
"+",
"+#### Q4.3: Leeres $models?",
"+",
"+**Fehlerbehandlung (ERGÄNZUNG):**",
"+```php",
"+\/\/ model-select.php",
"+<?php if (empty($models)): ?>",
"+ <select disabled class=\"form-select form-select--error\">",
"+ <option>Keine Modelle verfügbar - Sync erforderlich<\/option>",
"+ <\/select>",
"+<?php else: ?>",
"+ \/\/ Normaler Select",
"+<?php endif; ?>",
"+```",
"+",
"+#### Q4.4: Warum id=\"model\" hardcoded?",
"+",
"+**Problem:** Mehrere Instanzen auf einer Seite → ID-Konflikt.",
"+",
"+**KORREKTUR:**",
"+```php",
"+$id = $id ?? 'model_' . uniqid();",
"+```",
"+",
"+---",
"+",
"+### 11.5 Collection-Verwaltung und RAG-Konsistenz",
"+",
"+#### Q5.1: Verantwortung für vector_size?",
"+",
"+**Verantwortlichkeiten:**",
"+",
"+| Aktion | Verantwortlich | Prüfung |",
"+|--------|----------------|---------|",
"+| Collection erstellen | Pipeline\/Admin | Muss Embedding-Modell angeben |",
"+| Embedding-Modell wechseln | Admin | Re-Embedding erforderlich |",
"+| vector_size validieren | Sync | Qdrant ist Source of Truth |",
"+",
"+#### Q5.2: Inkompatible Embeddings bei Multi-Select?",
"+",
"+**KRITISCHE LÜCKE:**",
"+",
"+```php",
"+\/\/ CollectionValidator.php (NEU)",
"+public function validateSelection(array $collectionIds): ValidationResult",
"+{",
"+ $collections = $this->repository->findByIds($collectionIds);",
"+ $vectorSizes = array_unique(array_column($collections, 'vector_size'));",
"+",
"+ if (count($vectorSizes) > 1) {",
"+ return ValidationResult::error(",
"+ 'Inkompatible Collections: Unterschiedliche Vektorgrößen'",
"+ );",
"+ }",
"+",
"+ return ValidationResult::ok();",
"+}",
"+```",
"+",
"+#### Q5.3: embedding_model nur dokumentativ?",
"+",
"+**Aktuell:** Ja, rein informativ.",
"+",
"+**ERGÄNZUNG:** Validierung bei Suche:",
"+```php",
"+\/\/ QdrantService::search()",
"+public function search(array $collectionIds, string $query): array",
"+{",
"+ $embedding = $this->embedder->embed($query);",
"+",
"+ \/\/ Prüfen ob Embedding-Dimension passt",
"+ foreach ($collectionIds as $cid) {",
"+ $col = $this->repository->find($cid);",
"+ if ($col['vector_size'] !== count($embedding)) {",
"+ throw new DimensionMismatchException($cid, $col['vector_size'], count($embedding));",
"+ }",
"+ }",
"+}",
"+```",
"+",
"+#### Q5.4: Versteckte Qdrant-Collections?",
"+",
"+**Lösung über is_active:**",
"+```sql",
"+-- Qdrant hat 10 Collections, 7 sollen sichtbar sein",
"+UPDATE rag_collections SET is_active = FALSE WHERE collection_id IN ('internal_test', 'backup_2024');",
"+```",
"+",
"+#### Q5.5: System vs. Benutzer-Collections?",
"+",
"+**source_type ist ausreichend:**",
"+```",
"+source_type = 'system' → Interne Collections (entities, taxonomy)",
"+source_type = 'manual' → Benutzer-relevante",
"+source_type = 'nextcloud'→ Dokumente",
"+```",
"+",
"+---",
"+",
"+### 11.6 Python-Pipeline-Kopplung",
"+",
"+#### Q6.1: Warum direkte DB in Python, aber MCP sonst Pflicht?",
"+",
"+**Klarstellung:**",
"+",
"+| Kontext | Zugriff | Begründung |",
"+|---------|---------|------------|",
"+| Claude Code (CLI) | MCP | Audit, Sicherheit |",
"+| PHP Application | PDO | Standard-Pattern |",
"+| Python Pipeline | PyMySQL | Batch-Performance |",
"+",
"+**MCP-Pflicht gilt für Claude Code, nicht für Anwendungscode.**",
"+",
"+#### Q6.2: Schema-Drift erkennen?",
"+",
"+**Lösung:**",
"+```python",
"+# schema_check.py",
"+EXPECTED_COLUMNS = {",
"+ 'content_orders': ['id', 'contracts', 'structures', ...],",
"+}",
"+",
"+def verify_schema():",
"+ for table, columns in EXPECTED_COLUMNS.items():",
"+ actual = get_columns(table)",
"+ missing = set(columns) - set(actual)",
"+ if missing:",
"+ raise SchemaError(f\"Missing columns in {table}: {missing}\")",
"+```",
"+",
"+#### Q6.3: Concurrent Writes?",
"+",
"+**Aktuell:** Kein explizites Locking.",
"+",
"+**Analyse:** Unkritisch, weil:",
"+1. Admin-UI schreibt Metadaten (selten)",
"+2. Pipeline schreibt Content (häufig)",
"+3. Keine Überschneidung der Schreibziele",
"+",
"+**Bei Bedarf:** Row-Level Locking für kritische Updates.",
"+",
"+---",
"+",
"+### 11.7 User-Präferenzen und Werte-Hierarchie",
"+",
"+#### Q7.1: JSON-Validierung vor Speicherung?",
"+",
"+**LÜCKE - Validierung erforderlich:**",
"+",
"+```php",
"+public function update(int $userId, array $data): void",
"+{",
"+ \/\/ Validierung",
"+ if (isset($data['default_collections'])) {",
"+ $decoded = json_decode($data['default_collections'], true);",
"+ if (json_last_error() !== JSON_ERROR_NONE) {",
"+ throw new ValidationException('Invalid JSON for collections');",
"+ }",
"+ \/\/ Existenz prüfen",
"+ foreach ($decoded as $id) {",
"+ if (!$this->collectionRepository->exists($id)) {",
"+ throw new ValidationException(\"Collection not found: $id\");",
"+ }",
"+ }",
"+ }",
"+}",
"+```",
"+",
"+#### Q7.2: Alte Sessions vs. neue Defaults?",
"+",
"+**Kein Problem durch Design:**",
"+```",
"+Session speichert eigene Werte → unabhängig von Defaults",
"+Defaults gelten nur für NEUE Sessions",
"+```",
"+",
"+#### Q7.3: Defaults pro Modul?",
"+",
"+**Aktuell:** Global (ein Satz Defaults für Chat + Content).",
"+",
"+**Zukunftsoption:**",
"+```sql",
"+ALTER TABLE user_preferences",
"+ADD COLUMN scope ENUM('global', 'chat', 'content') DEFAULT 'global';",
"+```",
"+",
"+#### Q7.4: Korrupter Preferences-Datensatz?",
"+",
"+**Fallback in Repository:**",
"+```php",
"+public function getForUser(int $userId): array",
"+{",
"+ $row = $this->db->fetch(\"SELECT * FROM user_preferences WHERE user_id = ?\", [$userId]);",
"+",
"+ return $row ?: $this->createDefaultPreferences($userId);",
"+}",
"+",
"+private function createDefaultPreferences(int $userId): array",
"+{",
"+ $defaults = [...]; \/\/ System-Defaults",
"+ $this->db->insert('user_preferences', ['user_id' => $userId] + $defaults);",
"+ return $defaults;",
"+}",
"+```",
"+",
"+---",
"+",
"+### 11.8 Presets, Temperature und Tokens",
"+",
"+#### Q8.1: Temperature 0.0-1.0 Begrenzung?",
"+",
"+**Begründung:**",
"+- Anthropic: 0.0-1.0 (offiziell)",
"+- Ollama: modellabhängig, 1.0 sicherer Maximalwert",
"+- **Vereinheitlichung > Flexibilität**",
"+",
"+#### Q8.2: Token-Stufen prozentual vs. semantisch?",
"+",
"+**Prozentual gewählt weil:**",
"+- Modell-unabhängig",
"+- Keine Pflege semantischer Definitionen",
"+- User versteht \"50% vom Maximum\"",
"+",
"+**Alternative (nicht gewählt):**",
"+```",
"+Kurz: ~500 Tokens → \"Ein Absatz\"",
"+Mittel: ~2000 Tokens → \"Eine Seite\"",
"+Lang: ~4000 Tokens → \"Mehrere Seiten\"",
"+```",
"+",
"+#### Q8.3: Abweichung von Preset?",
"+",
"+**preset_id wird auf 'custom' gesetzt:**",
"+```php",
"+\/\/ Wenn Temperature manuell geändert:",
"+if ($submittedTemp !== $preset['temperature']) {",
"+ $data['preset_id'] = null; \/\/ oder 'custom'",
"+}",
"+```",
"+",
"+#### Q8.4: Parallelität default_preset_id + default_temperature?",
"+",
"+**INKONSISTENZ - Klarstellung erforderlich:**",
"+",
"+| Priorität | Quelle | Wann |",
"+|-----------|--------|------|",
"+| 1 | Session-Wert | Wenn vorhanden |",
"+| 2 | Preset → temperature | Wenn preset_id gesetzt |",
"+| 3 | default_temperature | Wenn kein Preset |",
"+",
"+**Empfehlung:** Nur `default_preset_id` speichern, Temperature daraus ableiten.",
"+",
"+---",
"+",
"+### 11.9 JavaScript-Steuerung",
"+",
"+#### Q9.1: slug vs. id für Presets?",
"+",
"+**Inkonsistenz bestätigt:**",
"+```js",
"+\/\/ Aktuell gemischt:",
"+data-preset-id=\"1\"",
"+data-preset-slug=\"precise\"",
"+```",
"+",
"+**KORREKTUR:** Nur `id` verwenden, `slug` nur für CSS-Klassen.",
"+",
"+#### Q9.2: API-Fehler bei Modellwechsel?",
"+",
"+**ERGÄNZUNG:**",
"+```js",
"+fetch(`\/api\/v1\/llm\/${modelId}`)",
"+ .then(r => {",
"+ if (!r.ok) throw new Error('API Error');",
"+ return r.json();",
"+ })",
"+ .catch(err => {",
"+ console.error('Model info not available');",
"+ \/\/ Fallback: Aktuelle Werte behalten",
"+ showToast('Modellinfo nicht verfügbar - Standardwerte werden verwendet');",
"+ });",
"+```",
"+",
"+#### Q9.3: Race Conditions?",
"+",
"+**ERGÄNZUNG - Debounce + State Lock:**",
"+```js",
"+let isUpdating = false;",
"+",
"+modelSelect.addEventListener('change', debounce(async function() {",
"+ if (isUpdating) return;",
"+ isUpdating = true;",
"+",
"+ try {",
"+ await updateTokenOptions(this.value);",
"+ } finally {",
"+ isUpdating = false;",
"+ }",
"+}, 300));",
"+```",
"+",
"+---",
"+",
"+### 11.10 Contracts: Migration und Semantik",
"+",
"+#### Q10.1: AND-Verknüpfung ohne Konfliktauflösung?",
"+",
"+**Design-Entscheidung:**",
"+- Contracts sind **additiv** (alle müssen erfüllt sein)",
"+- Widersprüche = Admin-Fehler, nicht System-Problem",
"+",
"+**Empfehlung:** Validierung bei Speicherung:",
"+```php",
"+public function validateContractCombination(array $contractIds): bool",
"+{",
"+ \/\/ Prüfen auf bekannte Konflikte",
"+ \/\/ z.B. \"Kurz & prägnant\" + \"Ausführlich & detailliert\"",
"+ \/\/ → Warnung, aber kein Fehler",
"+}",
"+```",
"+",
"+#### Q10.2: Widersprüchliche Contracts?",
"+",
"+**Keine technische Lösung, sondern Governance:**",
"+1. Admin-Schulung",
"+2. Contract-Beschreibungen klar formulieren",
"+3. Optionale Validierungsregeln in `content_config`",
"+",
"+#### Q10.3: Ausführungsreihenfolge?",
"+",
"+**Aktuell:** Keine definierte Reihenfolge.",
"+",
"+**Bei Bedarf:**",
"+```sql",
"+ALTER TABLE content_config",
"+ADD COLUMN priority INT DEFAULT 0;",
"+```",
"+",
"+#### Q10.4: Auto-Select nur bei Contracts?",
"+",
"+**Semantische Unterscheidung:**",
"+- **Contracts:** Qualitätsstandards → sollten immer gelten",
"+- **Structures:** Formatvorlagen → bewusste Wahl",
"+",
"+#### Q10.5: Migration Rollback?",
"+",
"+**Rollback-Script (ERGÄNZUNG):**",
"+```sql",
"+-- Rollback von JSON zu INT",
"+ALTER TABLE content_orders",
"+ADD COLUMN contract_id_old INT;",
"+",
"+UPDATE content_orders",
"+SET contract_id_old = JSON_EXTRACT(contracts, '$[0]')",
"+WHERE JSON_LENGTH(contracts) > 0;",
"+",
"+ALTER TABLE content_orders",
"+DROP COLUMN contracts;",
"+",
"+ALTER TABLE content_orders",
"+CHANGE contract_id_old contract_id INT;",
"+```",
"+",
"+---",
"+",
"+### 11.11 Structures und Template-Fusion",
"+",
"+#### Q11.1: Was bedeutet Template-Fusion?",
"+",
"+**Definition:**",
"+```",
"+Structure A: \"Verwende Markdown-Überschriften\"",
"+Structure B: \"Füge Zusammenfassung am Anfang ein\"",
"+",
"+Fusion: Beide Anweisungen werden im Prompt kombiniert",
"+```",
"+",
"+**Kein intelligentes Merging, sondern Konkatenation.**",
"+",
"+#### Q11.2: Konfliktmodell?",
"+",
"+**Keines - bewusste Entscheidung:**",
"+- Structures sind Vorschläge, keine harten Regeln",
"+- LLM interpretiert kombinierte Anweisungen",
"+- Bei echten Konflikten: Admin korrigiert Auswahl",
"+",
"+#### Q11.3: Structures im Chat sinnvoll?",
"+",
"+**Ja, für:**",
"+- \"Antworte immer mit Bullet Points\"",
"+- \"Verwende Codeblöcke für Beispiele\"",
"+- \"Strukturiere Antworten mit Überschriften\"",
"+",
"+---",
"+",
"+### 11.12 Migration und Betrieb",
"+",
"+#### Q12.1: Migrationsfahrplan?",
"+",
"+**Phase-Dependencies (ERGÄNZUNG):**",
"+",
"+```",
"+Phase 1 (DB-Schema) ──────────┐",
"+ ▼",
"+Phase 2 (Repositories) ───────┤",
"+ ▼",
"+Phase 3 (Admin-Seiten) ◄──────┤ (kann parallel)",
"+ ▼",
"+Phase 4 (Partials) ───────────┤",
"+ ▼",
"+Phase 5 (Integration) ────────┤",
"+ ▼",
"+Phase 6 (Testing) ────────────┘",
"+",
"+BLOCKING: Phase 1 muss vor allen anderen abgeschlossen sein",
"+PARALLEL: Phase 3 + 4 können gleichzeitig",
"+```",
"+",
"+#### Q12.2: Halb-migrierter Zustand erkennen?",
"+",
"+**ERGÄNZUNG - Migration-Status-Tabelle:**",
"+```sql",
"+CREATE TABLE migration_status (",
"+ id INT AUTO_INCREMENT PRIMARY KEY,",
"+ migration_name VARCHAR(100) NOT NULL UNIQUE,",
"+ phase INT NOT NULL,",
"+ status ENUM('pending', 'running', 'completed', 'failed') DEFAULT 'pending',",
"+ started_at DATETIME,",
"+ completed_at DATETIME,",
"+ error_message TEXT",
"+);",
"+```",
"+",
"+#### Q12.3: Wann alte Felder nicht mehr lesen?",
"+",
"+**Migrations-Contract:**",
"+```",
"+Phase 1.7-1.8: contract_id → contracts (DB)",
"+Phase 10.2: ContentController liest NUR contracts",
"+Phase 12.15: Test bestätigt: contract_id nicht mehr genutzt",
"+ → contract_id kann entfernt werden (optional)",
"+```",
"+",
"+---",
"+",
"+### 11.13 Governance und Verträge",
"+",
"+#### Q13.1: Normative Definition von Contracts\/Presets\/Structures?",
"+",
"+**LÜCKE - Dokumentation erforderlich:**",
"+",
"+```markdown",
"+# \/docs\/governance\/content-config-types.md",
"+",
"+## Contract",
"+- **Zweck:** Verbindliche Qualitätsregeln für Content",
"+- **Enforcement:** Critique-Phase prüft, Chat verwendet als Guideline",
"+- **Beispiel:** \"DSGVO-konform\", \"Akademischer Stil\"",
"+",
"+## Structure",
"+- **Zweck:** Formatvorlage für Ausgabe",
"+- **Enforcement:** Template für Generierung, optional im Chat",
"+- **Beispiel:** \"Blog-Artikel\", \"FAQ-Format\"",
"+",
"+## Preset",
"+- **Zweck:** Vordefinierte LLM-Parameter-Kombination",
"+- **Enforcement:** Setzt Temperature + Token automatisch",
"+- **Beispiel:** \"Präzise\", \"Kreativ\"",
"+```",
"+",
"+#### Q13.2: Widersprüchliche Defaults verhindern?",
"+",
"+**Aktuell:** Keine Validierung.",
"+",
"+**Empfehlung:**",
"+```php",
"+\/\/ PreferencesValidator.php",
"+public function validate(array $prefs): ValidationResult",
"+{",
"+ $errors = [];",
"+",
"+ \/\/ Prüfen: Default-Modell aktiv?",
"+ if ($prefs['default_model_id']) {",
"+ $model = $this->modelRepo->find($prefs['default_model_id']);",
"+ if (!$model || !$model['is_active']) {",
"+ $errors[] = 'Default-Modell ist nicht aktiv';",
"+ }",
"+ }",
"+",
"+ \/\/ Prüfen: Default-Collections existieren?",
"+ $collections = json_decode($prefs['default_collections'], true);",
"+ foreach ($collections as $id) {",
"+ if (!$this->collectionRepo->isActive($id)) {",
"+ $errors[] = \"Collection '$id' nicht verfügbar\";",
"+ }",
"+ }",
"+",
"+ return $errors ? ValidationResult::failed($errors) : ValidationResult::ok();",
"+}",
"+```",
"+",
"+#### Q13.3: System-Divergenz langfristig verhindern?",
"+",
"+**Maßnahmen:**",
"+",
"+1. **Architektur-Contracts:** Dieses Dokument als Referenz",
"+2. **Automated Tests:** Partials werden von beiden Views verwendet",
"+3. **Lint-Rules:** Verbieten direkter SQL außerhalb Repositories",
"+4. **Review-Prozess:** Änderungen an Partials reviewen",
"+",
"+```php",
"+\/\/ tests\/ArchitectureTest.php",
"+public function test_all_views_use_partials(): void",
"+{",
"+ $views = glob(VIEW_PATH . '\/{chat,content}\/*.php', GLOB_BRACE);",
"+",
"+ foreach ($views as $view) {",
"+ $content = file_get_contents($view);",
"+",
"+ \/\/ Kein direktes <select name=\"model\">",
"+ $this->assertStringNotContainsString(",
"+ '<select name=\"model\"',",
"+ $content,",
"+ \"View $view should use model-select.php partial\"",
"+ );",
"+ }",
"+}",
"+```",
"+",
"+---",
"+",
"+### 11.14 Zusammenfassung: Offene Aktionen",
"+",
"+| # | Aktion | Priorität | Status |",
"+|---|--------|-----------|--------|",
"+| 1 | FallbackStrategy-Klasse erstellen | Hoch | TODO |",
"+| 2 | llm_models: params_source, model_digest hinzufügen | Mittel | TODO |",
"+| 3 | llm_model_history Audit-Table erstellen | Niedrig | TODO |",
"+| 4 | CollectionValidator für Embedding-Kompatibilität | Hoch | TODO |",
"+| 5 | JSON-Validierung in UserPreferencesRepository | Hoch | TODO |",
"+| 6 | Debounce + State Lock in JS | Mittel | TODO |",
"+| 7 | Migration-Status-Tabelle erstellen | Hoch | TODO |",
"+| 8 | Governance-Dokumentation erstellen | Mittel | TODO |",
"+| 9 | ArchitectureTest für Partial-Nutzung | Niedrig | TODO |",
"+| 10 | Preset-ID vs. Slug Inkonsistenz beheben | Niedrig | TODO |"
]
}
],
"userModified": false,
"replaceAll": false
}
}