audit: Nacharbeit Umsetzungsaudit 25.09.2026 — Bildfreigabe, Beweisarchiv, KI-Atomarität, Berichte (Block 8, N01–N05, N07, N08, N10–N21, 5.1, 5.2)

Bilder (N01, N05, N07, N08): gemeinsamer Freigabezustand
PressReleaseImage::isPubliclyDeliverable() für Seiten, OG und Medienroute
(Bestand bleibt sichtbar, v2 nur mit Bestätigung, Admin-Upload bis zum
Kundenklick nicht öffentlich); eigene Ability confirmImageRights nur für den
Kontoinhaber, ohne Super-Admin-Kurzschluss, Identität und ausstehende
Bestätigung auch im Service; Status-Regel unter der PM-Sperre; Cache-Flush
nach dem Commit außerhalb der Dateibereinigung; Snapshot nach Bestätigung an
veröffentlichter PM; Bestätigungstexte an die gezeigte Kennung gebunden.

Beweisarchiv (N02, N10, N11, N12, N13, 5.1): Tombstone unter Zeilensperre mit
erneuter Hold-/Fristprüfung; Dateioperationen ausgewertet, Commands mit
FAILURE; record() über .tmp + move; Marker archive_pending_at mit
archive:retry-pending und archive:sweep-orphans; Anonymisierung entfernt Bilder
über den Remover mit Antragsbezug; Draft-Purge räumt Medien nie
veröffentlichter abgelehnter Einreichungen; App-Kopien fallen mit dem
Archiv-Tombstone; label_corrected nur für veröffentlichte Bilder;
archive_entries.legal_request_id; FileDeletion-Helfer; Systemstatus-Checks.

KI und Hintergrund (N03, N04, N14, N20, N21): Ergebnis-UPDATE und Audit in
einer Transaktion; erwarteter Hash bis in reject()/holdForManualReview();
Ablaufmail per beanspruchtem Marker in einer Transaktion; strikte
Schemaprüfung der Vision-Antwort; Fortschrittsmarke ungecacht.

Berichte und Canonical (N15–N19, 5.2): lückenlose Alterskohorten;
Feed-Identität mit Portal und Sprache; Sitemap-Rolle in der Kundenansicht;
Indexverteilung auf einer Grundgesamtheit; Kopien nach Wegfall der
Erstquelle neu bewertet; Kategorie-Admin leert Seiten-Caches; Dry-Run
zählt alle Tabellen.

Gesamtsuite: 2.122 Tests grün, einer übersprungen; Larastan und Pint sauber.
Zwei Migrationen (nullable Spalten, INSTANT). N09 offen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-25 14:26:49 +00:00
parent 34fa342c7f
commit 2a2215b3c3
66 changed files with 3092 additions and 368 deletions

View file

@ -88,6 +88,8 @@ class OpenAiImagePrecheckDriver implements ImagePrecheckDriver
throw new RuntimeException('OpenAI-Bildsichtung lieferte ein unvollständiges Schema.');
}
$this->assertSchema($decoded);
return ImagePrecheckResult::ok($decoded, 'openai', $model, null, [
'prompt_version' => (string) config('images.precheck.prompt_version'),
'sent_size' => [$prepared['width'], $prepared['height']],
@ -95,6 +97,41 @@ class OpenAiImagePrecheckDriver implements ImagePrecheckDriver
]);
}
/**
* Strikte Prüfung der Wertstruktur (Audit N20): Ein Schlüssel allein
* reicht nicht — alle Kategorien auf `null`, leere Objekte, String-
* Booleans („false" ist in PHP wahr) oder Konfidenzen außerhalb 0…1
* ergäben sonst ein scheinbar belastbares `ok` ohne Treffer. Formal
* unbrauchbare Antworten gehen als Ausnahme in den bestehenden
* `error`-Pfad (Block K.1: durchlassen mit Vermerk, nie blockieren).
*
* @param array<string, mixed> $decoded
*/
private function assertSchema(array $decoded): void
{
foreach (ImagePrecheckCategory::cases() as $category) {
$entry = $decoded[$category->value];
if (! is_array($entry)) {
throw new RuntimeException("OpenAI-Bildsichtung lieferte ein ungültiges Schema: {$category->value} ist kein Objekt.");
}
if (! array_key_exists('hit', $entry) || ! is_bool($entry['hit'])) {
throw new RuntimeException("OpenAI-Bildsichtung lieferte ein ungültiges Schema: {$category->value}.hit ist kein Boolean.");
}
$confidence = $entry['confidence'] ?? null;
if (! (is_int($confidence) || is_float($confidence)) || ! is_finite($confidence) || $confidence < 0 || $confidence > 1) {
throw new RuntimeException("OpenAI-Bildsichtung lieferte ein ungültiges Schema: {$category->value}.confidence liegt nicht in 0…1.");
}
if (array_key_exists('note', $entry) && $entry['note'] !== null && ! is_string($entry['note'])) {
throw new RuntimeException("OpenAI-Bildsichtung lieferte ein ungültiges Schema: {$category->value}.note ist kein Text.");
}
}
}
private function systemPrompt(): string
{
$keys = implode(', ', array_map(fn (ImagePrecheckCategory $c): string => $c->value, ImagePrecheckCategory::cases()));

View file

@ -124,7 +124,9 @@ final readonly class ImagePrecheckResult
}
$confidence = isset($entry['confidence']) && is_numeric($entry['confidence']) ? (float) $entry['confidence'] : 0.0;
$hit = (bool) ($entry['hit'] ?? false);
// Nur ein echtes `true` zählt (Audit N20): der String "false" wäre
// per Cast wahr. Die strikte Schemaprüfung sitzt im Treiber.
$hit = ($entry['hit'] ?? false) === true;
$note = isset($entry['note']) && is_string($entry['note']) && trim($entry['note']) !== '' ? mb_substr(trim($entry['note']), 0, 200) : null;
$normalized[$category->value] = [