ki/quota: Hash-Bindung atomar bis in den Publish-Lock, roter Re-Check lehnt ab, Kontingent-Altbestand (Block 7, U02–U04)

- Jobs speichern Ergebnis nur per bedingtem UPDATE auf den analysierten Hash;
  publish() prüft bei ki/scheduler unter der Zeilensperre die jüngste gültige
  Klassifikation und plant sonst eine neue Prüfung ein (StaleClassificationException)
- Re-Check ohne Routing: Rot bei einer PM in Review lehnt wie die Erstprüfung ab
- consumePublishSlot() zählt eine NULL-Periode wie den laufenden Monat;
  Backfill-Migration setzt die Periode für Altverbrauch
- ApiPortalResolutionTest: gemeinsame Kategorie wegen der U10-Prüfung

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-18 12:22:22 +00:00
parent eb95b3b830
commit 082e6bb76c
11 changed files with 698 additions and 25 deletions

View file

@ -10,6 +10,7 @@ use App\Models\PressRelease;
use App\Scopes\PortalScope;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseService;
use App\Services\PressRelease\StaleClassificationException;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Throwable;
@ -30,6 +31,11 @@ use Throwable;
* zum aktuellen Text (Admin- oder Korrektur-Bearbeitung nach der Prüfung),
* wird nicht veröffentlicht, sondern eine Prüfung mit Routing eingeplant –
* Grün geht dann zum fälligen Termin live, Rot lehnt ab und benachrichtigt.
* Die Vorprüfung hier ist nur der schnelle Filter; verbindlich prüft
* PressReleaseService::publish() unter der Zeilensperre noch einmal
* (Audit U02) und meldet eine Abweichung als StaleClassificationException –
* die zählt hier ebenfalls als Re-Check, nicht als Fehler; publish() hat die
* Prüfung dann schon eingeplant.
* Technische Fehler ergeben einen Fehler-Exit-Code, damit der Failure-Hook
* des Schedulers greift (Audit F20); fachliche Ablehnungen nicht.
*/
@ -115,6 +121,9 @@ class PublishScheduledPressReleases extends Command
} catch (BlacklistViolationException $e) {
$rejected++;
$this->warn($line.' [REJECT: '.$e->word.']');
} catch (StaleClassificationException) {
$rechecked++;
$this->warn($line.' [RECHECK: Inhalt unter der Sperre abweichend, Prüfung neu eingeplant]');
} catch (Throwable $e) {
$failed++;
$this->error($line.' [FAIL: '.$e->getMessage().']');