# Entwicklungsplan: KI-Prüfung & Veröffentlichungs-Pipeline Stand: 11.06.2026 — **Phasen 0–5 abgeschlossen**, Phase 6 (Trust-Score) offen. Dieser Plan definiert die schrittweise Umsetzung der automatisierten Prüfung und Veröffentlichung von Pressemitteilungen (PM). Er ist so geschnitten, dass jede Phase einzeln umgesetzt, getestet und ausgeliefert werden kann. > **Abgleich mit dem Decision-Update (11.06.2026)**: Das > [`Decision-Update Preisstruktur & Veröffentlichungs-Flow`](../Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md) > setzt auf dieser Pipeline auf und ergänzt zum Launch drei noch offene > Flow-Regeln, die **nicht** Teil dieses Plans waren: > > 1. **Submit-Gate**: „Zur Prüfung einreichen" wird hinter eine aktive > Buchung gelegt (das Modal zeigt ohne Buchung einen Buchungs-Hinweis). > 2. **Slot-Verbrauch bei Veröffentlichung** statt bei Einreichung — > rot abgelehnte PMs verbrauchen keinen Slot. Der aktuelle Quota-Stub > zählt noch beim Einreichen (`submitForReview`) und muss umgestellt werden. > 3. **Kein Re-Check zum Launch**: eine Einreichung = eine Prüfung; > Nachbessern + erneut prüfen kommt erst in Phase 2. > 4. **Gelb-Routing geändert (Entscheidung 12.06.2026)**: Gelb geht zum > Launch **direkt live** wie Grün — keine manuelle Review-Queue mehr. > Nur Rot wird abgelehnt (mit Begründung an den Autor). Phase 4 unten > beschreibt das ursprünglich gebaute Verhalten (Gelb → manuelle Queue); > die Umstellung erfolgt im Phase-9-Plan > (`docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md`, Päckchen 9A). ## Ziel & Leitprinzip Jede eingehende PM wird **automatisch von einer KI geprüft**. Nur in **äußersten Fällen** erfolgt eine manuelle redaktionelle Prüfung. Zwei Einreichungsstellen müssen denselben Prüf-Pfad durchlaufen: 1. **Web-Formular** (Customer- und Admin-Editor) 2. **API** (`/api/v1/press-releases`) Es gibt zwei voneinander unabhängige Bewertungen (Konzept-Update 1, Abschnitt 15): - **Klassifikations-Score (Grün/Gelb/Rot)** — der „Red Flag": entscheidet, ob überhaupt veröffentlicht wird. **Jetzt umzusetzen.** - **Content-Score (0–100) → Stufe (Standard/Geprüft/Hochwertig)** — die Qualitätsbewertung („Scoring"). **Spätere Phase.** ## Konzept-Grundlage - `docs/konzept/Konzept-Update 1 – Überarbeitete Abschnitte.md`, §15.1 (Klassifikations-Score) und §15.2 (Content-Score). - `docs/konzept/Konzept-Update 2 – Score-Stufen-System.md` (Stufen-Mapping, `content_tier`, Außenkommunikation). Klassifikations-Score laut Konzept §15.1: | Klassifikation | Bedeutung | Auswirkung | |---|---|---| | **Grün** | unauffällig | direkte Veröffentlichung (optional 5–10 Min. Verzögerung) | | **Gelb** | unklar/grenzwertig | manuelle Review-Queue (nicht boostbar) | | **Rot** | unzulässig | zurück an Autor mit Begründung, keine Veröffentlichung | Faktoren (Red Flags): Werbung statt PM, beleidigend/diskriminierend, rechtlich heikel, Spam-Muster, unseriöse Versprechen. Speicherung laut Konzept: `press_releases.classification` plus Audit-Log `ki_audits`. ## Ist-Zustand (Bestandsaufnahme) - **Statuswerte** (`App\Enums\PressReleaseStatus`): `draft`, `review`, `published`, `rejected`, `archived`. - **Web-Einreichung**: `App\Services\PressRelease\PressReleaseService::submitForReview()` prüft nur eine wortbasierte Blacklist (`config/blacklist.php` via `BlacklistService`), setzt sonst Status `review`, erhöht das Quota und schreibt ein `PressReleaseStatusLog`. - **Veröffentlichung**: `PressReleaseService::publish()` (Admin-Aktion) und der Cron `App\Console\Commands\PublishScheduledPressReleases` (publiziert `review`-PMs mit fälligem `scheduled_at`). Beide prüfen erneut nur die Blacklist. - **API**: `App\Http\Controllers\Api\V1\PressReleaseController::store()` / `update()` schreiben `status` direkt aus dem Request (erlaubt: `draft`, `review`) und rufen `submitForReview` **nicht** auf. Eine API-PM mit `status=review` landet damit **ohne** Blacklist-/Quota-/Log-Prüfung in der Queue. - **UI-Einreichung (Prozess-Start)**: - Detailansicht ([show.blade.php]): vollständiges Modal `confirm-submit-review` mit rechtlichen Hinweisen, Quota-Anzeige und Bestätigungs-Checkboxen → ruft `submitForReview`. - Bearbeiten ([edit.blade.php]): Button „Speichern & zur Prüfung" mit nur `wire:confirm` (Browser-Dialog), **kein** Modal. - Erstellen ([create.blade.php]): Button „Zur Prüfung senden" ohne Modal. - **Kein** Datenmodell für Klassifikation/Score: keine Spalten `classification`, `content_score`, `content_tier`, keine Tabelle `ki_audits`. ## Lücken & Risiken - **L1 — API-Bypass**: Einreichung über die API umgeht jede Prüfung. - **L2 — Keine echte Inhaltsprüfung**: nur eine triviale Wort-Blacklist; keine Erkennung von Werbung, Spam, rechtlich heiklen oder unseriösen Inhalten. - **L3 — Auto-Publish ohne Klassifikation**: geplante PMs werden vom Cron veröffentlicht, ohne dass eine inhaltliche Bewertung stattgefunden hat. - **L4 — Uneinheitlicher Prozess-Start**: das Bestätigungs-Modal existiert nur in der Detailansicht, nicht beim Bearbeiten/Erstellen. - **L5 — Kein Audit**: KI-Entscheidungen wären ohne `ki_audits` nicht nachvollziehbar (DSGVO / Nachweispflicht). ## Zielarchitektur ``` Einreichung (Formular ODER API) │ ▼ SubmissionService.submit() ← ein einziger Funnel │ ├─ Hard-Filter: Blacklist (synchron, deterministisch) ▼ ClassificationService.classify() ← KI (Claude), mit Fallback │ ├─ Rot → status=rejected, Begründung an Autor ├─ Gelb → status=review (manuelle Queue, „äußerste Fälle") └─ Grün → Veröffentlichungspfad (sofort / geplant) │ ▼ ki_audits (vollständiges Audit-Log jeder KI-Entscheidung) + press_releases.classification / classified_at + (später) content_score / content_tier ``` Kernregeln: - Formular **und** API rufen ausschließlich `SubmissionService.submit()` auf. Die API darf `status` nicht mehr frei setzen; `published` ist über die API nie erreichbar. - Re-Klassifikation bei jeder Änderung einer PM (Konzept §15.1: „Bei Änderung der PM wird neu klassifiziert"). - Schwellen/Verhalten sind konfigurierbar (`config/scoring.php`), damit sie ohne Code-Änderung kalibriert werden können. --- ## Entwicklungsschritte ### Phase 0 — Prozess-Start im UI vereinheitlichen — ✅ erledigt (11.06.2026) **Ziel:** Das bestehende Einreichungs-Modal erscheint überall dort, wo eine PM eingereicht wird — auch beim Bearbeiten (Button „Speichern & zur Prüfung") und beim Erstellen (Button „Zur Prüfung senden"). Reiner UI-Schritt, kein Backend. **Umsetzung:** Das Modal `confirm-submit-review` (rechtliche Hinweise, Quota, Bestätigungs-Checkboxen) wird in Customer-Show, -Create **und** -Edit über `flux:modal.trigger` geöffnet; bestätigt ruft es wie geplant `submitForReview` bzw. `saveAndSubmit`/`save('review')`. **Umfang:** - Modal `confirm-submit-review` aus `show.blade.php` in eine wiederverwendbare Blade-/Volt-Komponente extrahieren (z. B. `resources/views/livewire/components/press-release-submit-modal.blade.php`). - In `edit.blade.php` den `wire:confirm`-Button durch einen `flux:modal.trigger` ersetzen; bei Bestätigung wird wie bisher `saveAndSubmit` ausgeführt (erst speichern, dann einreichen). - In `create.blade.php` denselben Modal-Trigger vor `save('review')` schalten. - Texte/Checkboxen identisch zur Detailansicht halten (rechtliche Hinweise, Quota, Bestätigungen). **Betroffene Dateien:** `resources/views/livewire/customer/press-releases/{show,edit,create}.blade.php`, neue Komponente unter `resources/views/livewire/components/`. **Done:** In allen drei Ansichten (Customer: show/edit/create) öffnet derselbe Bestätigungsdialog; Tests für Edit/Create-Submit grün. **Admin-Editor (`/admin/press-releases/`) — bewusst ausgenommen:** Der Admin-Editor behält sein bisheriges Verhalten (`wire:confirm`). Begründung: Wenn eine PM beim Admin landet, hat die vorgelagerte User-Prüfung (Einreichungs-Modal im Customer-Flow) bereits stattgefunden. Der Admin braucht hier keinen erneuten Bestätigungsdialog. Stattdessen erhält der Admin-Editor in einer späteren Phase einen zusätzlichen **„Prüfung"-Button** (siehe Phase 4: On-Demand-KI-Prüfung). **Tests:** Volt-Tests, die das Öffnen des Modals und den Submit-Pfad (`saveAndSubmit` / `save('review')`) abdecken. ### Phase 1 — Einreichungs-Funnel & API-Absicherung — ✅ erledigt (11.06.2026) **Ziel:** Beide Einreichungsstellen laufen durch einen Pfad; die API-Lücke (L1) wird geschlossen. Noch ohne KI — nur Vereinheitlichung. **Umsetzung:** - `PressReleaseService::submitForReview()` ist der alleinige Einreichungs-Einstieg (Web-Formular **und** API rufen dieselbe Methode). Auf eine separate `SubmissionService`-Fassade wurde bewusst verzichtet — `submitForReview` ist bereits die stabile Schnittstelle, in die Phase 3 die KI-Klassifikation einhängt. - API: `status` aus den Validierungsregeln von `StorePressReleaseRequest` und `UpdatePressReleaseRequest` entfernt (inkl. ungenutzter Imports). `store()` erzeugt jetzt **immer** `PressReleaseStatus::Draft`; ein übergebenes `status` wird ignoriert. `update()` kann den Status nicht mehr setzen. - Neue explizite Route `POST /api/v1/press-releases/{pressRelease}/submit` (`press-releases.submit`) → `PressReleaseController::submit()`. Diese prüft `press-releases:write`, Ownership und erlaubt nur `draft`/`rejected` (sonst 409); ruft `submitForReview()`; eine `BlacklistViolationException` wird als **422** mit Begründung zurückgegeben. Damit greifen Blacklist-, Quota- und Status-Log-Behandlung auch für API-Einreichungen. - `published` ist über die API weiterhin nie erreichbar (nur Admin-Aktion/Cron). **Betroffene Dateien:** `app/Http/Controllers/Api/V1/PressReleaseController.php`, `app/Http/Requests/Api/V1/{Store,Update}PressReleaseRequest.php`, `routes/api.php`. `PressReleaseService` blieb unverändert (Schnittstelle ausreichend). **Done:** API kann keine PM mehr ungeprüft in `review` heben; eine PM-Einreichung verhält sich über API und Formular identisch. **Tests:** `tests/Feature/Api/V1/PressReleaseSubmitApiTest.php` (Create erzeugt immer Draft & ignoriert `status`; Submit-Route hebt nach `review`, zählt Quota, schreibt Log; Blacklist → 422 + `rejected`; fehlende Schreibrechte → 403; bereits in `review` → 409; fremde PM → 403). Alle grün. ### Phase 2 — Datenmodell & Audit — ✅ erledigt (11.06.2026) **Ziel:** Persistenz für Klassifikation und vollständiges KI-Audit. Noch ohne Verhaltensänderung (alle Felder nullable). **Umsetzung:** - Migration `add_classification_to_press_releases`: Spalten `classification` (string(16), nullable, nach `status`) und `classified_at` (timestamp, nullable) plus Index auf `classification`. `content_score`/`content_tier` bewusst **erst in Phase 5** (siehe Datenmodell-Anhang). - Migration `create_ki_audits_table`: `press_release_id` (FK, cascade), `type`, `provider` (nullable), `model` (nullable), `result` (nullable), `reason` (text, nullable), `raw_response` (json, nullable), `created_at` (useCurrent), Index `(press_release_id, type)`. Kein `updated_at` (append-only Log). - Model `App\Models\KiAudit` (`$timestamps = false`, Cast `raw_response` → array, Konstanten `TYPE_CLASSIFICATION`/`TYPE_CONTENT_SCORE`, Relation `pressRelease()`), Relation `PressRelease::kiAudits()` (neueste zuerst). - Enum `App\Enums\PressReleaseClassification` (Green/Yellow/Red + `label()`), in `PressRelease::casts()` für `classification` registriert. - `config/scoring.php`: Anbieter/Modell-Auswahl (`CLASSIFICATION_PROVIDER`, Default `deterministic`, `CLASSIFICATION_MODEL`), Timeout, Grün-Verzögerung (Minuten), Gelb→manuelle-Queue-Flag sowie Content-Score-Stufen-Schwellen (Phase 5). - `KiAuditFactory` mit States `classification()` / `contentScore()`. **Betroffene Dateien:** zwei neue Migrationen unter `database/migrations/`, `app/Models/PressRelease.php`, `app/Models/KiAudit.php`, `app/Enums/PressReleaseClassification.php`, `config/scoring.php`, `database/factories/KiAuditFactory.php`. **Done:** Migrationen laufen; Modelle/Casts/Relation vorhanden; keine bestehende Funktionalität verändert (alle Felder nullable). **Tests:** `tests/Feature/PressReleaseClassificationModelTest.php` (Enum-/ Datetime-Cast, Default null, `kiAudits()`-Reihenfolge, `raw_response`-Array-Cast + Relation, Cascade-Delete). Alle grün. ### Phase 3 — KI-Klassifikation (Red Flag) — ✅ erledigt (11.06.2026) **Ziel:** Echte inhaltliche Prüfung jeder Einreichung; Ergebnis asynchron als Klassifikation gespeichert und auditiert. **Entscheidungen (11.06.2026):** Erster aktiver Anbieter ist **OpenAI** (Key/ Budget vorhanden); Anthropic/Gemini folgen über dieselbe Treiber-Schnittstelle. Klassifikation läuft **asynchron über die Queue** (synchron wäre später nicht handelbar). Zum Testen ohne Dauer-Worker gibt es einen Drain-Befehl. **Umsetzung:** - **Provider-agnostische Treiber-Architektur** unter `app/Services/PressRelease/Classification/`: - Interface `Contracts\ClassificationDriver::classify(PressRelease): ClassificationResult`. - `ClassificationResult` (Value Object: Enum-Klassifikation, `reasons[]`, `provider`, `model`, `rawResponse`, `reasonText()`). - `Drivers\OpenAiClassificationDriver` — OpenAI Chat-Completions via `Http`-Client, liest `config/services.openai` (Key/URL/Modell/Timeout), erzwingt `response_format: json_object` und parst `{classification, reasons[]}`. Wirft bei fehlendem Key / HTTP-Fehler / ungültigem JSON. - `Drivers\DeterministicClassificationDriver` — Blacklist → Rot/Grün (nie Gelb), als Fallback ohne externe API. - `ClassificationManager` (Laravel-Manager) löst den Treiber aus `config('scoring.classification.provider')` auf (`createOpenaiDriver`/`createDeterministicDriver`). - **Asynchroner Job** `app/Jobs/ClassifyPressRelease` (Queue `classification`, `tries=3`): klassifiziert über den aktiven Treiber, bei Ausfall **Fallback** auf den deterministischen Treiber (mit `Log::warning`), schreibt `press_releases.classification`/`classified_at` und einen `ki_audits`-Eintrag (inkl. `provider`/`model`/`reason`/`raw_response`). - **Einbindung in den Funnel:** `PressReleaseService::submitForReview()` stößt nach dem synchronen Blacklist-Hard-Filter und dem Statuswechsel den Job an (`ClassifyPressRelease::dispatch(...)->onQueue('classification')`). Greift für Formular **und** API (gemeinsamer Einstieg aus Phase 1). - **Drain-Befehl** `php artisan classification:work` (Option `--once`): arbeitet die Queue einmalig ab und beendet sich (`queue:work --stop-when-empty`) — zum Testen ohne permanenten Worker. - **Konfig:** `config/scoring.php` Default-Provider auf `openai` gesetzt (`CLASSIFICATION_PROVIDER`); Modell leer ⇒ `config('services.openai.model')`. - **Test-Isolation:** `phpunit.xml` erzwingt `CLASSIFICATION_PROVIDER=deterministic` und leeren `OPENAI_API_KEY`, damit die Suite keine echten OpenAI-Calls macht; der OpenAI-Pfad wird gezielt mit `Http::fake()` getestet. **Noch offen (bewusst):** Re-Klassifikation bei jeder PM-Änderung (Update über Formular/API) ist noch **nicht** verdrahtet — Phase 3 klassifiziert beim Einreichen. Nachzuziehen, wenn das Status-Routing (Phase 4) steht. Anthropic-/ Gemini-Treiber + SDK folgen separat. **Betroffene Dateien:** `app/Services/PressRelease/Classification/*`, `app/Jobs/ClassifyPressRelease.php`, `app/Console/Commands/RunClassificationQueue.php`, `app/Services/PressRelease/PressReleaseService.php`, `config/scoring.php`, `phpunit.xml`. **Done:** Jede Einreichung (Formular + API) stößt asynchron eine Klassifikation an, erzeugt einen `ki_audits`-Eintrag; bei KI-Ausfall greift der deterministische Fallback nachvollziehbar. Status-Routing folgt in Phase 4. **Tests:** `tests/Feature/PressReleaseClassificationJobTest.php` (OpenAI grün/gelb mit `Http::fake`, Fallback bei HTTP-500, deterministisch Rot bei Blacklist, Dispatch auf Queue `classification` via `Queue::fake`). Alle grün; volle Suite 416 grün (2 vorbestehende WIP-Failures unverändert). ### Phase 4 — Routing, Auto-Publish & Review-Queue — ✅ erledigt (11.06.2026) **Ziel:** Die Klassifikation steuert den Status. Manuelle Prüfung nur noch bei Gelb. **Umsetzung:** - **Routing im Job** über `PressReleaseService::routeByClassification()` (vom `ClassifyPressRelease`-Job nach dem Klassifizieren aufgerufen): - **Rot** → `reject(..., source: 'ki')`: `status=rejected`, KI-Begründung per Mail an den Autor (`PressReleaseRejected`, wie bei Blacklist). - **Gelb** → keine Aktion, bleibt `review` (manuelle Admin-Queue). - **Grün** → `autoPublishGreen()`: ohne Termin sofort veröffentlichen, optional mit Sicherheitsfenster `scoring.classification.green_delay_minutes` (über `published_at`-Override); mit zukünftigem `scheduled_at` bleibt die PM in `review` und der Scheduler publiziert zum Termin. - Greift nur, solange die PM noch `review` ist (manuelle Admin-Eingriffe haben Vorrang). `publish()` erhielt einen `?Carbon $publishedAtOverride`-Parameter, `reject()` einen `string $source`-Parameter. - **Scheduler** `PublishScheduledPressReleases`: Kandidaten-Query um `where('classification', 'green')` erweitert — nur **grüne** fällige PMs werden automatisch publiziert; gelbe warten immer auf den Admin. Geplante Termine werden weiterhin respektiert. - **Admin-Review-Queue:** Index- und Show-Ansicht zeigen ein KI-Klassifikations- Badge (grün/gelb/rot); der Index hat einen **Klassifikations-Filter** (`classificationFilter`, inkl. URL-Param, Active-Chip, Reset) — damit „nur Gelb" filterbar. Die Show-Ansicht blendet im Review-Block den **KI-Hinweis** (Begründung aus dem jüngsten `ki_audits`-Eintrag) ein. **Test-Isolation (wichtig):** Da Tests mit `sync`-Queue den Job inline ausführen, wurde der Klassifikations-Job in den „submit→review"-Tests via `Queue::fake()` entkoppelt (Workflow, PublishModal, API-Submit). Die Scheduler-Tests setzen jetzt `classification = green` für Publish-Kandidaten; neuer Test: fällige **gelbe** PM bleibt `review`. **Betroffene Dateien:** `app/Services/PressRelease/PressReleaseService.php`, `app/Jobs/ClassifyPressRelease.php`, `app/Console/Commands/PublishScheduledPressReleases.php`, Admin-Views `resources/views/livewire/admin/press-releases/{index,show}.blade.php`. **Done:** Grüne PMs gehen automatisch live (sofort/zum Termin), rote werden abgelehnt + Autor benachrichtigt, nur gelbe landen in der manuellen Queue; Admin sieht Klassifikation + KI-Begründung und kann nach Gelb filtern. **Tests:** Routing in `PressReleaseClassificationJobTest` (Rot→rejected+Mail, Grün-sofort→published+Mail, Grün-geplant→bleibt review, Gelb→bleibt review); Scheduler in `PressReleaseSchedulingTest` (grün fällig→published, gelb fällig→review); Admin-UI in `PressReleaseIndexPhase8bTest` (KI-Badge, Klassifikations-Filter). Volle Suite 423 grün (2 vorbestehende WIP-Failures). **Admin „Prüfung"-Button (On-Demand-KI-Prüfung)** — ✅ erledigt (11.06.2026): - Im Admin-Editor gibt es oben den Button **„Prüfung"**, der ein Modal `admin-ki-check` öffnet: auswählbare Klassifikation (Content-Score als „in Vorbereitung" deaktiviert) und ein **Anbieter-Override** (Konfiguriert / OpenAI / Deterministisch). - `runKiCheck()` dispatcht `ClassifyPressRelease` auf der Queue `classification` **mit `route: false`** und optionalem `providerOverride`. Das ist eine nachgelagerte Re-Check-Prüfung: sie aktualisiert nur `classification` + `ki_audits`, **ohne** den Status zu ändern (kein Auto-Publish/Reject) — die Entscheidung bleibt beim Admin (Ergebnis sichtbar in der Detailansicht). - Dafür erhielt `ClassifyPressRelease` die Parameter `bool $route = true` und `?string $providerOverride = null`. **Tests:** `tests/Feature/Admin/AdminKiCheckTest.php` (Button/Modal sichtbar; Dispatch mit `route=false` + Provider-Override; Abbruch ohne Auswahl; Re-Check-Job aktualisiert Bewertung, lässt Status unverändert). **Re-Klassifikation bei Änderung** (Konzept §15.1) — ✅ erledigt (11.06.2026): - Neue Service-Methode `reclassifyIfClassified()`: dispatcht – nur wenn die PM bereits klassifiziert ist – `ClassifyPressRelease` mit `route: false` (Re-Check ohne Statusänderung). - Eingehängt überall dort, wo Inhalt geändert wird, und nur bei tatsächlicher Änderung von Titel/Text (`wasChanged(['title', 'text'])`): Customer-Editor `save()`, Admin-Editor `save()`, API `update()`. Beim Einreichen übernimmt weiterhin `submitForReview` die (routende) Klassifikation. **Tests:** `tests/Feature/PressReleaseReclassifyTest.php` (Service dispatcht nur bei vorhandener Klassifikation; API-Update klassifiziert neu bei Text-Änderung, nicht bei reiner Keyword-Änderung). **Noch offen / Folgearbeiten:** - **Live-Aktualisierung der Ansicht** nach Abschluss des Hintergrund-Jobs (Polling/Event) wäre ein optionales UX-Upgrade; aktuell erscheint das Ergebnis nach Reload/Navigation. - **Content-Score-Option** im Prüfungs-Modal — ✅ mit Phase 5 aktiviert (s. u.). ### Phase 5 — Content-Score & Stufen — ✅ erledigt (11.06.2026) **Ziel:** Qualitätsbewertung 0–100 → Stufe Standard/Geprüft/Hochwertig (Konzept-Update 2). **Umsetzung:** - **Datenmodell:** Migration `add_content_score_to_press_releases` — `content_score` (tinyint, nullable), `content_tier` (string, nullable, Index), `scored_at`. Enum `App\Enums\PressReleaseContentTier` (Standard/Geprueft/Hochwertig) mit `fromScore()` (Schwellen aus `config/scoring.php`), `label()` und `isPubliclyBadged()` (Standard wird laut Update 2 nicht beworben). In `PressRelease` als Cast registriert. - **Schwellen** (`config/scoring.php`): Geprüft ≥ 60, Hochwertig ≥ 80 (Update 2), kalibrierbar; plus Anbieter/Modell/Timeout für den Score. - **Treiber-Architektur** unter `app/Services/PressRelease/ContentScore/` analog zur Klassifikation: `Contracts\ContentScoreDriver`, `ContentScoreResult`, `Drivers\OpenAiContentScoreDriver` (gewichtete Faktoren §15.2 als JSON `{score, breakdown}`), `Drivers\DeterministicContentScoreDriver` (regelbasierte Heuristik: Länge, Bild, Quelle, Headline, Vollständigkeit), `ContentScoreManager`. - **Job** `app/Jobs/ScorePressRelease` (Queue `classification`, Fallback auf deterministisch): schreibt `content_score` + abgeleitete `content_tier` + `scored_at` und `ki_audits` (type=content_score). Optionaler `providerOverride`. - **Berechnung** bei Einreichung (`submitForReview` dispatcht Klassifikation **und** Score) und bei Inhaltsänderung (`rescoreIfScored()` in Customer-/ Admin-Editor und API-`update()`, analog zur Re-Klassifikation). - **Anzeige:** - Customer-Editor: Score-Panel (Punktzahl, Stufe, „noch X Punkte bis zur nächsten Stufe") — der produktive Editor-Score laut Update 2. - Admin-Index & -Show: Stufen-/Score-Badge (intern inkl. Punktzahl). - Customer-Detailansicht: öffentliches Stufen-Badge (✓ Geprüft / ★ Hochwertig; Standard ohne Badge). - **Admin-Prüfungs-Modal:** Content-Score-Option aktiviert; `runKiCheck()` dispatcht zusätzlich `ScorePressRelease`. **Done:** Score wird bei Einreichung/Änderung berechnet, Stufe abgeleitet, auditiert und überall sichtbar. **Tests:** `tests/Feature/PressReleaseContentScoreTest.php` (Tier-Mapping, öffentliche Badges, OpenAI-Score→Tier+Audit, Fallback, Dispatch bei Submit, Re-Score nur wenn bereits bewertet); Editor-Panel in `CustomerPressReleaseEditPhase7Test`; Stufen-Badge in `PressReleaseIndexPhase8bTest`; Content-Score-Dispatch in `AdminKiCheckTest`. Volle Suite 440 grün. **Noch offen / Folgearbeiten:** - **Public Web-Frontend** (presseecho/businessportal24): Stufen-Badges in den öffentlichen Listen/Detailseiten gemäß Update 2 ergänzen (bisher nur im Portal/Backend und der Customer-Ansicht). - **Score-History & Breakdown-Ansicht** (Publisher-Dashboard) und Boost- Eligibilität (Abschnitt 16) sind eigene spätere Ausbaustufen. ### Phase 6 — Trust-Score (später) Account-/Firmen-Ebene (Konzept §15.3): lockert die KI-Freigabe-Schwelle für zuverlässige Publisher. Eigene spätere Ausbaustufe; hier nur als Ausblick vermerkt. --- ## Datenmodell-Anhang (Zielzustand) ``` press_releases (Ergänzungen) + classification enum(green,yellow,red) NULL + classified_at timestamp NULL + content_score tinyint NULL (Phase 5) + content_tier enum(standard,gepruft,hochwertig) NULL (Phase 5) ki_audits (neu) - id - press_release_id FK - type enum(classification,content_score) - provider string (z. B. anthropic) - model string (z. B. claude-opus-4-8) - result string/json - reason text NULL - raw_response json/longtext - created_at timestamp ``` ## Offene Entscheidungen - **Anbieter & Modell** — ✅ entschieden (11.06.2026): Erster aktiver Anbieter ist **OpenAI** (`CLASSIFICATION_PROVIDER=openai`, Modell aus `config/services.openai`). Architektur provider-agnostisch; Anthropic/Gemini folgen. Offen bleibt, ob später mehrere Anbieter parallel (Primär + Fallback jenseits des deterministischen) laufen sollen. - **Synchron vs. Queue** — ✅ entschieden (11.06.2026): **Queue** (asynchron, Queue-Name `classification`). Drain zum Testen: `php artisan classification:work`. - **Dependency**: OpenAI-Treiber nutzt den nativen `Http`-Client (kein neues Composer-Paket). Anthropic PHP-SDK (`anthropic-ai/sdk`) ist **freigegeben**; für Gemini je ein offizielles/etabliertes SDK oder HTTP-Client bei Umsetzung des Treibers. - **Grün-Verzögerung**: 0 Min. (sofort) oder 5–10 Min. (Konzept-Option) als Sicherheitsfenster — konfigurierbar über `scoring.classification.green_delay_minutes`, Default noch festzulegen. - **Gelb-Verhalten**: ausschließlich manuelle Queue, oder zusätzlich automatische Benachrichtigung des Autors. - **DSGVO**: Aufbewahrung/Anonymisierung der `raw_response` in `ki_audits`. ## Nächste Schritte Phasen 0–5 sind umgesetzt (Suite grün). Es folgen: 1. **Launch-Block aus dem Decision-Update** (siehe Abgleich-Box oben): Submit-Gate hinter Buchung, Slot-Verbrauch bei Veröffentlichung, Tarif-/Zahlungs-Modul. 2. **Betrieb**: Queue-Worker für `classification` im Produktions-Setup (Test-Drain: `php artisan classification:work`). 3. **Folgearbeiten**: Live-Aktualisierung des KI-Ergebnisses in der UI, Stufen-Badges im öffentlichen Web-Frontend, Anthropic-/Gemini-Treiber. 4. **Phase 6**: Trust-Score (eigene Ausbaustufe).