docs(catalog): Review-Befunde in die KAT-01-Spec einarbeiten
search_normalize für alle API-Rollen ausführbar, Symbole vor unaccent entfernen, eine Basisfunktion gemeinsam mit #13, Reihenfolge 3a nach 2b, Spec-F4 mit vorläufiger Messung ohne Staging, Indizes nicht als Begründung, Grenzen und Hinweise aus dem Review. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a1895f4c5f
commit
b65ec0efe7
1 changed files with 171 additions and 110 deletions
|
|
@ -3,12 +3,12 @@
|
|||
| | |
|
||||
|---|---|
|
||||
| Issue | #3 (KAT-01) |
|
||||
| Status | Entwurf, wartet auf Freigabe durch Chris (Spec-F1 bis Spec-F7 in § 9.2 offen, keine blockiert) |
|
||||
| Status | Entwurf, wartet auf Freigabe durch Chris (Spec-F1 bis Spec-F7 in § 9.2 offen, keine blockiert); Review (code-reviewer) eingearbeitet, § 10 |
|
||||
| Autor | architect |
|
||||
| Stand | 2026-11-03 |
|
||||
| Etappe | R1.1 Fundament |
|
||||
| Abhängigkeiten | #2 KAT-05 Teil 2a (gemergt: `catalog_games`, `publishers`, `game_publishers`, `manual_fields`, `GameCover`, Startbestand-Werkzeug). Teilabhängigkeit: Krit. 6 verweist auf #13 KAT-04 (später). Von #3 hängen ab: #4 KAT-02 (Einstieg in die Detailseite), #13 KAT-04 (Melden aus dem Leerzustand), #29 QUE-09 (Startseite ohne Anmeldung zeigt die Suche), #14 KTO-02 (Lieblingsspiele per Katalogsuche). |
|
||||
| Quellen | Issue #3 inkl. Kommentare, `requirements/20-r1-akzeptanzkriterien.md` § KAT-01 und § Querschnitt, `requirements/r1-klaerungen.md` (3, 4, 13, 14, 15, 36, O6, O7), `requirements/backlog.md` (Datenmodell-Änderungen 3, 4), `docs/specs/2-katalog-daten.md` (E3, E4, E6, 3.2, § 5, 9.5), `docs/datenmodell-r1.md`, `supabase/migrations/20261102100200_catalog.sql`, `…/20261103142204_catalog_foundation.sql`, `supabase/catalog/build_startbestand.ts`, `app/lib/core/widgets/game_cover.dart`, `app/lib/core/routing/*`, `app/lib/features/auth/presentation/providers/auth_redirect.dart`, `docs/design-system.md`, `e2e/README.md`, ADR-0001, ADR-0002 |
|
||||
| Abhängigkeiten | #2 KAT-05 Teil 2a (gemergt: `catalog_games`, `publishers`, `game_publishers`, `manual_fields`, `GameCover`, Startbestand-Werkzeug); Teil 2b (`feature/2b-katalog-zusammenfuehren`) wird vor 3a gemergt (3.3). Teilabhängigkeit: Krit. 6 verweist auf #13 KAT-04 (später). Von #3 hängen ab: #4 KAT-02 (Einstieg in die Detailseite), #13 KAT-04 (Melden aus dem Leerzustand, `search_normalize`), #29 QUE-09 (Startseite ohne Anmeldung zeigt die Suche), #14 KTO-02 (Lieblingsspiele per Katalogsuche). |
|
||||
| Quellen | Issue #3 inkl. Kommentare, `requirements/20-r1-akzeptanzkriterien.md` § KAT-01 und § Querschnitt, `requirements/r1-klaerungen.md` (3, 4, 13, 14, 15, 36, O6, O7), `requirements/backlog.md` (Datenmodell-Änderungen 3, 4), `docs/specs/2-katalog-daten.md` (E3, E4, E6, 3.2, § 5, 9.5), `docs/specs/13-fehlendes-spiel-melden.md` (Branch `docs/13-kat04-spec`), `docs/datenmodell-r1.md`, `supabase/migrations/20261102100200_catalog.sql`, `…/20261103142204_catalog_foundation.sql`, `supabase/catalog/build_startbestand.ts`, `supabase/tests/database/06_catalog_foundation.test.sql`, `…/07_catalog_merge.test.sql` (2b), `app/lib/core/widgets/game_cover.dart`, `app/lib/core/routing/*`, `app/lib/features/auth/presentation/providers/auth_redirect.dart`, `docs/design-system.md`, `e2e/README.md`, ADR-0001, ADR-0002 |
|
||||
|
||||
**Namenskonvention:** „Spec-F1“ bis „Spec-F7“ sind die Fragen an Chris aus § 9.2. „E1“ usw. sind Kernentscheidungen dieser Spec. „Spec #2 E3“ verweist auf Entscheidungen der Katalog-Spec. „B1“, „F1“, „U1“, „T1“, „D1“, „C1“ sind Aufgaben (§ 8).
|
||||
|
||||
|
|
@ -24,17 +24,17 @@ Nicht im Scope (Issue): Barcode-Suche, Offline-Katalog (O7), Empfehlungen. Ebenf
|
|||
|---|---|---|
|
||||
| E1 | **Eine RPC `search_catalog_games`** (PostgreSQL-Funktion, `security invoker`, `stable`) liefert Treffer, Gesamtzahl und Verlage in **einer** Anfrage. Sie liest über die View `catalog_games` (Spec #2 E3), damit ausgeblendete und zusammengeführte Spiele nie erscheinen und die Anzeige-Maskierung (Spec #2 E6) gilt. | Relevanz über Titel, alternative Titel und Verlage, UND-Filter über Zuordnungstabellen und Normalisierung lassen sich mit PostgREST-Filtern auf der View nicht sauber ausdrücken. Eine Anfrage je Seite hält Krit. 7 (100 ms RTT) ein. `security invoker` heißt: RLS gilt für den Aufrufer, die Funktion umgeht nichts. |
|
||||
| E2 | **Alternative Titel als eigene Tabelle `game_alt_titles`** (Titel + Sprache `de`/`en`), nur lesbar für Clients, Pflege über Studio und das Startbestand-Werkzeug (Spalte `alt_titles` der CSV, Spec #2 4.3). Neuer `manual_fields`-Schlüssel `alt_titles`. | Klärung 3, Backlog „Datenmodell-Änderungen“ Nr. 3. Nutzer haben in R1 keinen Anwendungsfall zum Schreiben; ohne Schreibrecht entfällt die Angriffsfläche. |
|
||||
| E3 | **Normalisierung in der Datenbank:** eine Funktion `public.search_normalize(text)` (Kleinschreibung, Akzente und Umlaute entfernt, alles außer `a–z`/`0–9` wird zu einem Leerzeichen). Ergebnis als **gespeicherte generierte Spalten** `games.title_search`, `game_alt_titles.title_search`, `publishers.name_search` mit Trigramm-Index. | Krit. 1 „unabhängig von Groß- und Kleinschreibung und Sonderzeichen“. Eine zentrale Funktion statt Logik im Client; #13 nutzt sie für „gleicher normalisierter Titel“ (Klärung 15) wieder. Generierte Spalten können nicht vergessen werden und brauchen keine Trigger. |
|
||||
| E4 | **Treffer-Regel:** Der Suchbegriff wird in Wörter zerlegt; **jedes Wort** muss als Teilzeichenkette im Titel, in einem alternativen Titel oder in einem Verlagsnamen des Spiels vorkommen. Zusätzlich trifft der Begriff ohne Leerzeichen („7wonders“) den Titel ohne Leerzeichen. | Krit. 1 („ein Titel oder ein Teil davon“), Krit. 2, Krit. 3 in einem Suchfeld. „siedler catan“ findet „Die Siedler von Catan“, „catan kosmos“ findet Catan von Kosmos. |
|
||||
| E3 | **Eine Basisfunktion `public.search_normalize(text)`** für alle normalisierten Titel (Symbole entfernen, Akzente/Umlaute entfernen, Kleinschreibung, alles außer `a–z`/`0–9` zu einem Leerzeichen), **angelegt von #3**, ausführbar für alle API-Rollen. Ergebnis als **gespeicherte generierte Spalten** `games.title_search`, `game_alt_titles.title_search`, `publishers.name_search`. #13 baut `normalize_game_title` darauf auf (3.2 Nr. 2). | Krit. 1 „unabhängig von Groß- und Kleinschreibung und Sonderzeichen“. Eine zentrale Funktion statt Logik im Client und statt zweier abweichender Normalisierungen in #3 und #13. Generierte Spalten können nicht vergessen werden und brauchen keine Trigger. |
|
||||
| E4 | **Treffer-Regel:** Der Suchbegriff wird in Wörter zerlegt; **jedes Wort** (ab 2 Zeichen) muss als Teilzeichenkette im Titel, in einem alternativen Titel oder in einem Verlagsnamen des Spiels vorkommen. Zusätzlich trifft der Begriff ohne Leerzeichen („7wonders“, „aeons end“) den Titel ohne Leerzeichen. | Krit. 1 („ein Titel oder ein Teil davon“), Krit. 2, Krit. 3 in einem Suchfeld. „siedler catan“ findet „Die Siedler von Catan“, „catan kosmos“ findet Catan von Kosmos. |
|
||||
| E5 | **Relevanz als feste Stufen** (genau gleich → beginnt mit → Wortanfang → enthält → nur über Verlag), danach Trigramm-Ähnlichkeit, Titel, ID. | Krit. 1 „nach Relevanz sortiert“ wird damit testbar und deterministisch; die eindeutige Reihenfolge ist Voraussetzung für stabiles Nachladen (Krit. 5). |
|
||||
| E6 | **Nachladen über `limit`/`offset`**, Seitengröße 30, Gesamtzahl über `count(*) over ()`. | Der Katalog ändert sich selten (Studio-Pflege), die Reihenfolge ist eindeutig (E5). Keyset-Paging über eine zusammengesetzte Relevanz wäre deutlich komplexer ohne spürbaren Nutzen bei einigen hundert bis tausend Spielen. |
|
||||
| E7 | **Filter sind UND-verknüpft**, auch mehrere gewählte Genres oder Mechaniken untereinander (Spec-F1). Filter auf Spielerzahl, Dauer und Alter schließen Spiele mit fehlender Angabe aus (Spec-F2). | Wörtliche Lesart von Krit. 4/8 „zu allen gewählten Filtern passen“ (Klärung 13). |
|
||||
| E8 | **Ohne Suchbegriff und ohne Filter zeigt die Suche den ganzen Katalog alphabetisch** (Spec-F3). | Krit. 8 (Entdecken) ohne Sonderfall: „keine Filter“ ist der leere Filter. Bei ~200 Spielen nützlich zum Stöbern. |
|
||||
| E8 | **Ohne Suchbegriff und ohne Filter zeigt die Suche den ganzen Katalog alphabetisch** (Spec-F3). | Krit. 8 (Entdecken) ohne Sonderfall: „keine Filter“ ist der leere Filter. Bei ~200 Spielen nützlich zum Stöbern. Der Katalog ist öffentlich; dass er sich vollständig durchblättern lässt, ist gewollt. |
|
||||
| E9 | **Neues Feature `features/catalog/`** mit Domäne (`CatalogGameSummary`, `CatalogSearchQuery`, `CatalogTerm`, `CatalogFailure`), Repository und einem Notifier mit eigenem Seitenzustand. **Alle Datenklassen sind nur lesend** (`createToJson: false`); das Feature schreibt nichts in `games`. | Merkpunkt aus Spec #2 § 10: Kein ungefiltertes `toJson()` darf an `insert`/`update` gehen. Ohne `toJson` ist das hier nicht nur dokumentiert, sondern unmöglich. |
|
||||
| E10 | **Öffentliche Route `/catalog`** mit Suchbegriff und Filtern als Query-Parameter (Web-Deep-Links). Die Startseite bekommt nur einen Einstieg; die Startseite ohne Anmeldung baut #29. | Web-URLs (ADR-0001, go_router); `privateRoutePaths` bleibt leer (Spec #1). |
|
||||
| E11 | **Krit. 6 in #3 als Leerzustand mit Hinweis und Einstiegspunkt**; die Aktion „Spiel melden“ wird mit #13 freigeschaltet (Spec-F4). | Issue: „Teilabhängigkeit: Kriterium 6 verweist auf KAT-04 (#13), das später umgesetzt wird“. |
|
||||
| E12 | **Krit. 7 wird mit einem eigenen, nur lesenden Playwright-Projekt `perf` gegen Staging gemessen** (Chrome-Drosselung 100 ms / 10 Mbit/s, mindestens 100 Suchen, Messung vom letzten Tastendruck bis zum ersten sichtbaren Treffer). Datenbank-Budget: p95 ≤ 150 ms bei 10 000 Spielen. | Klärung 14. Die bestehenden E2E-Helfer (`db.ts`, `E2E_ALLOWED_HOSTS`) sind bewusst nur lokal; Staging wird nur über die Web-App angesprochen. |
|
||||
| E13 | **Drei PRs:** 3a Backend, 3b App (parallel gegen den Vertrag aus § 4.2), 3c E2E und Messung. Nur 3c schließt das Issue. | Wie bei #2 (PR #52/#53); 3b kann mit gemocktem Repository sofort beginnen. |
|
||||
| E12 | **Krit. 7 wird mit einem eigenen, nur lesenden Playwright-Projekt `perf` gemessen** (Chrome-Drosselung 100 ms / 10 Mbit/s, mindestens 100 Suchen, Messung vom letzten Tastendruck bis zum ersten sichtbaren Treffer). **Staging gibt es noch nicht** (ADR-0002: ein Hetzner-Server); bis dahin vorläufige Messung gegen den lokalen bzw. CI-Stack mit Startbestand und synthetischem Volumen, die Staging-Messung wird Freigabebedingung in der Release-Checkliste (Spec-F4). Datenbank-Budget: p95 ≤ 150 ms bei 10 000 Spielen, gemessen als `anon` mit RLS. | Klärung 14. Die bestehenden E2E-Helfer (`db.ts`, `E2E_ALLOWED_HOSTS`) sind bewusst nur lokal; Staging wird nur über die Web-App angesprochen. |
|
||||
| E13 | **Drei PRs:** 3a Backend, 3b App (parallel gegen den Vertrag aus § 4.2), 3c E2E und Messung. Nur 3c schließt das Issue (unter Vorbehalt von Spec-F4). | Wie bei #2 (PR #52/#53); 3b kann mit gemocktem Repository sofort beginnen. |
|
||||
| E14 | **Keine neuen Dart-Pakete, keine Edge Function.** Neu sind nur die PostgreSQL-Erweiterungen `pg_trgm` und `unaccent` (Bestandteil des Supabase-Postgres-Images, auch self-hosted; Prüfung in B5). | CLAUDE.md, ADR-0002. Kein ADR nötig (§ 9.4). |
|
||||
|
||||
## 2. Akzeptanzkriterien
|
||||
|
|
@ -52,12 +52,12 @@ Nicht im Scope (Issue): Barcode-Suche, Offline-Katalog (O7), Empfehlungen. Ebenf
|
|||
|
||||
**Konkretisierungen dieser Spec (zur Freigabe):**
|
||||
|
||||
- **Krit. 1 „Sonderzeichen“** umfasst Satz- und Sonderzeichen (`:`, `-`, `®`, `'` usw.) und diakritische Zeichen (`é` → `e`, `ä` → `a`, `ß` → `ss`). Die Schreibweise „ae“ für „ä“ findet in R1 **nicht** automatisch „ä“ (Spec-F5).
|
||||
- **Krit. 1 „Teil davon“:** Die Suche beginnt ab **2 Zeichen** (nach Entfernen von Leerzeichen am Rand). Ein einzelnes Zeichen wirkt wie „kein Suchbegriff“ (§ 5.4).
|
||||
- **Krit. 1 „Sonderzeichen“** umfasst Satz- und Sonderzeichen (`:`, `-`, `'`, `’` usw.), Marken- und Schutzzeichen (`®`, `©`, `™`, `℗`, `℠`, werden ersatzlos entfernt) und diakritische Zeichen (`é` → `e`, `ä` → `a`, `ß` → `ss`, `Æ` → `ae`). Die Schreibweise „ae“ für „ä“ findet in R1 **nicht** automatisch „ä“ (Spec-F5).
|
||||
- **Krit. 1 „Teil davon“:** Die Suche beginnt ab **2 Zeichen** (App: nach Entfernen von Leerzeichen am Rand; RPC: nach Normalisierung). Weniger wirkt wie „kein Suchbegriff“ (§ 4.2, § 5.4).
|
||||
- **Krit. 4 Filtersemantik:** siehe § 4.2 und Spec-F2.
|
||||
- **Krit. 5:** Seitengröße 30. Nachgeladen wird automatisch kurz vor dem Listenende **und** über eine Schaltfläche „Weitere laden“ am Listenende (Tastatur und Screenreader, QUE-06 Krit. 4).
|
||||
- **Krit. 5:** Seitengröße 30. Nachgeladen wird automatisch kurz vor dem Listenende **und** über eine Schaltfläche „Weitere laden“ am Listenende (Tastatur und Screenreader, QUE-06 Krit. 4). Nachgeladen wird höchstens bis Versatz 10 000 (Grenze der RPC); bei der Katalogsgröße von R1 nicht erreichbar.
|
||||
- **Krit. 6:** siehe E11 und Spec-F4.
|
||||
- **Krit. 7 Messpunkt:** vom letzten Tastendruck (inklusive Entprellung von 300 ms) bis der erste Treffer sichtbar ist; Staging mit eingespieltem Startbestand (§ 7.3).
|
||||
- **Krit. 7 Messpunkt:** vom letzten Tastendruck (inklusive Entprellung von 300 ms) bis der erste Treffer sichtbar ist; Umgebung und Abnahme nach Spec-F4 (§ 7.3).
|
||||
|
||||
**Zuordnung zu den PRs**
|
||||
|
||||
|
|
@ -65,12 +65,12 @@ Nicht im Scope (Issue): Barcode-Suche, Offline-Katalog (O7), Empfehlungen. Ebenf
|
|||
|---|---|
|
||||
| 3a Backend | 1, 2, 3, 4, 5, 8 (Datenbankanteil, pgTAP); 7 (Datenbank-Budget, B5) |
|
||||
| 3b App | 1–6, 8 (App-Anteil, Unit/Widget); KAT-05 Krit. 10 (Platzhalter-Cover in der Trefferliste) |
|
||||
| 3c E2E/Messung | 1–6, 8 (Web-E2E); 7 (Messung gegen Staging, Abnahme durch Chris) |
|
||||
| 3c E2E/Messung | 1–6, 8 (Web-E2E); 7 (Messung nach Spec-F4) |
|
||||
|
||||
**Relevante Querschnittskriterien**
|
||||
|
||||
- **Sicherheit der Daten Krit. 2:** Suche ohne Anmeldung; die RPC ist für `anon` ausführbar und liefert nur Katalogspiele, nie private Spiele (auch nicht die eigenen). Nichts ist schreibbar.
|
||||
- **Datenschutz (QUE-04):** Suchbegriffe werden nicht gespeichert und nicht protokolliert (weder App-Logger noch Datenbank). Die RPC wird per `POST` aufgerufen (Standard von `supabase_flutter`), der Suchbegriff steht damit nicht in Zugriffsprotokollen von URLs. Keine Such-Analytik.
|
||||
- **Datenschutz (QUE-04):** Suchbegriffe werden nicht gespeichert und nicht protokolliert (weder App-Logger noch Datenbank; B5 prüft die Protokoll-Einstellungen der Datenbank). Die RPC wird per `POST` aufgerufen (Standard von `supabase_flutter`), der Suchbegriff steht damit nicht in Zugriffsprotokollen von URLs. Keine Such-Analytik.
|
||||
- **Sprachen (QUE-05) Krit. 2, 3:** alle Texte aus ARB (de/en); Genre- und Mechaniknamen nach Oberflächensprache (`name_de`/`name_en`); Trefferzahl mit Pluralform.
|
||||
- **Barrierefreiheit (QUE-06) Krit. 1–4:** Suchfeld und Filter beschriftet, Trefferzahl wird angesagt, Touch-Ziele ≥ 48 dp, 200 % Schrift, Tastaturbedienung im Web inkl. „Weitere laden“.
|
||||
- **Zustände, Fehler und Netz Krit. 1–3:** Lade-, Leer-, Fehler- (mit „Erneut versuchen“) und Normalzustand; Offline-Hinweis; Suchbegriff und Filter bleiben bei Fehlern erhalten.
|
||||
|
|
@ -83,9 +83,9 @@ Nicht im Scope (Issue): Barcode-Suche, Offline-Katalog (O7), Empfehlungen. Ebenf
|
|||
| Bedarf | Bestand nach 2a | Handlung |
|
||||
|---|---|---|
|
||||
| Nur sichtbare Katalogspiele (KAT-05 Krit. 4) | View `catalog_games` | RPC liest darüber (E1) |
|
||||
| Normalisierter Titel (Krit. 1) | Index `games_catalog_listed_idx` auf `lower(title)` | `search_normalize`, generierte Spalte `games.title_search` + Trigramm-Index (bestehender Index bleibt unverändert) |
|
||||
| Normalisierter Titel (Krit. 1) | Index `games_catalog_listed_idx` auf `lower(title)` | `search_normalize`, generierte Spalte `games.title_search` (bestehender Index bleibt unverändert) |
|
||||
| Alternative Titel mit Sprache (Krit. 2) | – (CSV-Spalte `alt_titles` reserviert, Spec #2 4.3) | Tabelle `game_alt_titles` |
|
||||
| Verlage (Krit. 3) | `publishers`, `game_publishers` | generierte Spalte `publishers.name_search` + Trigramm-Index |
|
||||
| Verlage (Krit. 3) | `publishers`, `game_publishers` | generierte Spalte `publishers.name_search` |
|
||||
| Spielerzahl, Dauer, Alter (Krit. 4) | Spalten an `games` | keine |
|
||||
| Genre, Mechanik (Krit. 4, 8) | `genres`, `mechanics`, `game_genres`, `game_mechanics` mit Index auf `genre_id`/`mechanic_id` und `unique (game_id, …)` | keine |
|
||||
| Kennzeichnung manuell gepflegt | `manual_fields`, Trigger `game_links_mark_manual(tg_argv)` | Schlüssel `alt_titles` ergänzen, Trigger an `game_alt_titles` |
|
||||
|
|
@ -96,12 +96,14 @@ Nur additiv. Es gilt die Rechte-Regel aus Spec #2 3.2: Jede neue Tabelle zuerst
|
|||
|
||||
**Grants je Objekt (verbindlich)**
|
||||
|
||||
| Objekt | `anon` | `authenticated` | Hinweis |
|
||||
|---|---|---|---|
|
||||
| `game_alt_titles` | `select` | `select` | kein Schreibrecht für API-Rollen; Policy nur für `select` |
|
||||
| `search_normalize(text)` | – | – | intern (generierte Spalten, RPC, später #13) |
|
||||
| `search_catalog_games(…)` | **`execute`** | **`execute`** | **erste Funktion im Projekt, die API-Rollen ausdrücklich ausführen dürfen**; `service_role` und `public` bleiben entzogen |
|
||||
| `games`, `publishers` | unverändert | unverändert | neue generierte Spalten sind über das bestehende Tabellenrecht `select` lesbar; die Spaltenrechte für `insert`/`update` (Spec #2 3.2) bleiben unverändert und enthalten die neuen Spalten nicht |
|
||||
| Objekt | `anon` | `authenticated` | `service_role` | Hinweis |
|
||||
|---|---|---|---|---|
|
||||
| `game_alt_titles` | `select` | `select` | – | kein Schreibrecht für API-Rollen; Policy nur für `select` |
|
||||
| `search_normalize(text)` | **`execute`** | **`execute`** | **`execute`** | rein rechnend, ohne Tabellenzugriff, deshalb auch als RPC harmlos. **Pflicht:** Generierte Spalten werden mit den Rechten des Schreibenden berechnet, und die RPC läuft als `security invoker`. Ohne `execute` scheitern Anlegen und Ändern privater Spiele durch Nutzer (`title_search`) und jede Suche als `anon`. |
|
||||
| `search_catalog_games(…)` | **`execute`** | **`execute`** | – | erste Abfrage-RPC des Projekts für API-Rollen; `public` und `service_role` bleiben entzogen |
|
||||
| `games`, `publishers` | unverändert | unverändert | unverändert | neue generierte Spalten sind über das bestehende Tabellenrecht `select` lesbar; die Spaltenrechte für `insert`/`update` (Spec #2 3.2) bleiben unverändert und enthalten die neuen Spalten nicht |
|
||||
|
||||
Rechte auf `extensions.unaccent` und `extensions.similarity` bestehen für die API-Rollen bereits (Supabase-Standard für das Schema `extensions`); B5 bestätigt das im Test.
|
||||
|
||||
**1. Erweiterungen**
|
||||
|
||||
|
|
@ -110,15 +112,38 @@ create extension if not exists pg_trgm with schema extensions;
|
|||
create extension if not exists unaccent with schema extensions;
|
||||
```
|
||||
|
||||
Beide gehören zum Supabase-Postgres-Image und zu PostgreSQL-contrib. **Unverifizierte Annahme (B5 prüft lokal und auf Staging mit `select * from pg_available_extensions where name in ('pg_trgm','unaccent')`):** Beide sind im self-hosted Image verfügbar und im Schema `extensions` anlegbar. Rückfall siehe R1.
|
||||
Beide gehören zum Supabase-Postgres-Image und zu PostgreSQL-contrib. **Unverifizierte Annahme (B5 prüft lokal und – sobald es Staging gibt – dort mit `select * from pg_available_extensions where name in ('pg_trgm','unaccent')`):** Beide sind im self-hosted Image verfügbar und im Schema `extensions` anlegbar. Rückfall siehe R1.
|
||||
|
||||
**2. Funktion `public.search_normalize(p text) returns text`**
|
||||
**2. Basisfunktion `public.search_normalize(p text) returns text`** (E3)
|
||||
|
||||
> **Gemeinsame Normalisierung (gleichlautend in Spec #3 und Spec #13):** `public.search_normalize(text)` ist die einzige Basisfunktion für normalisierte Titel. Sie wird von #3 (PR 3a) angelegt, ist rein rechnend und für `anon`, `authenticated` und `service_role` ausführbar, weil generierte Spalten und `security invoker`-Funktionen sie mit den Rechten des Aufrufers berechnen. #13 legt `public.normalize_game_title(text)` darüber an und entfernt nur dort Artikel; `search_normalize` entfernt nie Artikel.
|
||||
|
||||
- `language sql`, `immutable`, `strict`, `parallel safe`, `set search_path = ''`.
|
||||
- Ablauf: `extensions.unaccent('extensions.unaccent'::regdictionary, p)` (die Variante mit ausdrücklichem Wörterbuch, damit die Funktion als `immutable` gelten darf) → `lower(…)` → `regexp_replace(…, '[^a-z0-9]+', ' ', 'g')` → `btrim(…)` → `nullif(…, '')`.
|
||||
- Beispiele (Pflichtfälle im Test): `'Die Siedler von Catan'` → `'die siedler von catan'`; `'CATAN®'` → `'catan'`; `'7 Wonders: Duel'` → `'7 wonders duel'`; `'Café Größe'` → `'cafe grosse'`; `'Ticket to Ride – Europe'` → `'ticket to ride europe'`; `'!!!'` und `''` → `null`.
|
||||
- Ablauf, in dieser Reihenfolge:
|
||||
1. Marken- und Schutzzeichen entfernen: `regexp_replace(p, '[®©™℗℠]', '', 'g')` (sonst macht `unaccent` daraus `(R)`, `(C)`, `TM` und „CATAN®“ würde zu `catan r`),
|
||||
2. `extensions.unaccent('extensions.unaccent'::regdictionary, …)` mit festem Wörterbuch,
|
||||
3. `lower(…)`,
|
||||
4. `regexp_replace(…, '[^a-z0-9]+', ' ', 'g')`,
|
||||
5. `btrim(…)`,
|
||||
6. `nullif(…, '')`.
|
||||
- **Zu `immutable`:** `unaccent` selbst ist nur `stable`, weil das Wörterbuch geändert werden könnte. Der Wrapper mit ausdrücklich angegebenem Wörterbuch als `immutable` ist das übliche Vorgehen und funktioniert für generierte Spalten. Das verbleibende Risiko ist eine geänderte `unaccent.rules`-Datei nach einem Image-Upgrade; dafür gibt es eine Prüfabfrage (R6).
|
||||
- **Brüche** (`½`, `¼`): bewusst so, wie `unaccent` sie umschreibt (`½` → `1/2` → `1 2`). Eine Suche nach „1/2“ oder „1 2“ findet sie; eine Sonderbehandlung gibt es nicht.
|
||||
- **Apostroph** (`'`, `’`): wird wie jedes Sonderzeichen zu einem Leerzeichen (`Æon’s End` → `aeon s end`). Die Suche „aeons end“ findet den Titel trotzdem über die Regel ohne Leerzeichen (`qc`, § 4.2).
|
||||
- **Pflichtfälle im Test:**
|
||||
|
||||
| Eingabe | Ergebnis |
|
||||
|---|---|
|
||||
| `'Die Siedler von Catan'` | `'die siedler von catan'` |
|
||||
| `'CATAN®'` | `'catan'` |
|
||||
| `'Spiel™ ½'` | `'spiel 1 2'` |
|
||||
| `'Æon’s End'` | `'aeon s end'` (Suche `'aeons end'` trifft über `qc`) |
|
||||
| `'7 Wonders: Duel'` | `'7 wonders duel'` |
|
||||
| `'Café Größe'` | `'cafe grosse'` |
|
||||
| `'Ticket to Ride – Europe'` | `'ticket to ride europe'` |
|
||||
| `'!!!'`, `''` | `null` |
|
||||
|
||||
- Zeichen außerhalb des lateinischen Alphabets (z. B. japanische Titel) werden entfernt; in R1 gibt es nur deutsche und englische Titel (Spec-F7).
|
||||
- **Regel für spätere Änderungen:** Ändert sich die Funktion, müssen die generierten Spalten neu berechnet werden (Spalte in einer neuen Migration entfernen und neu anlegen). Steht als Kommentar an der Funktion.
|
||||
- **Regel für spätere Änderungen:** Ändert sich die Funktion (oder das Wörterbuch, R6), müssen die generierten Spalten neu berechnet werden (Spalte in einer neuen Migration entfernen und neu anlegen). Steht als Kommentar an der Funktion.
|
||||
|
||||
**3. Generierte Spalten und Indizes**
|
||||
|
||||
|
|
@ -128,7 +153,10 @@ Beide gehören zum Supabase-Postgres-Image und zu PostgreSQL-contrib. **Unverifi
|
|||
| `publishers` | `name_search` | `text generated always as (public.search_normalize(name)) stored` | `publishers_name_search_trgm_idx using gin (name_search extensions.gin_trgm_ops)` |
|
||||
| `game_alt_titles` | `title_search` | wie `games.title_search` | `game_alt_titles_title_search_trgm_idx using gin (title_search extensions.gin_trgm_ops)` |
|
||||
|
||||
Hinweise: Das Anlegen einer gespeicherten Spalte schreibt `games` einmal neu (unkritisch bei der Größe). Generierte Spalten kann kein Client setzen. Der Trigger `games_mark_manual_fields` arbeitet mit einer festen Spaltenliste und ist nicht betroffen. Die View `catalog_games` bleibt unverändert; die RPC verbindet sie per Primärschlüssel mit `games`, um `title_search` zu lesen.
|
||||
Hinweise:
|
||||
|
||||
- **Die Indizes sind keine Begründung für die Geschwindigkeit.** Bei Suchwörtern mit 2 Zeichen, bei der Regel ohne Leerzeichen (`replace(…)`), bei `or` über `exists`-Unterabfragen und mit den RLS-Prüfungen für `anon` nutzt der Planer sie weitgehend nicht. Bei der Größe von R1 reicht ein sequenzieller Scan; maßgeblich ist die Messung in B5 (§ 4.2 Leistung). Der Partial-Index auf `games` passt nur zu Abfragen mit den Bedingungen der View `catalog_games` (Kommentar in der RPC).
|
||||
- Das Anlegen einer gespeicherten Spalte schreibt `games` einmal neu (unkritisch bei der Größe). Generierte Spalten kann kein Client setzen. Der Trigger `games_mark_manual_fields` arbeitet mit einer festen Spaltenliste und ist nicht betroffen. Die View `catalog_games` bleibt unverändert; die RPC verbindet sie per Primärschlüssel mit `games`, um `title_search` zu lesen.
|
||||
|
||||
**4. Neue Tabelle `game_alt_titles`** (Klärung 3, E2)
|
||||
|
||||
|
|
@ -144,6 +172,7 @@ Hinweise: Das Anlegen einer gespeicherten Spalte schreibt `games` einmal neu (un
|
|||
- `unique (game_id, locale, title_search)` (Konfliktziel für das Startbestand-Werkzeug; derselbe Titel in derselben Sprache nur einmal, unabhängig von Schreibweise). Mehrere Titel je Sprache sind erlaubt (z. B. alter und neuer deutscher Titel).
|
||||
- Der Index der Unique-Constraint deckt Abfragen nach `game_id` ab; kein zusätzlicher Index nötig.
|
||||
- Trigger `game_links_mark_manual after insert or update or delete … execute function public.game_links_mark_manual('alt_titles')` (Funktion aus 2a, generisch über `tg_argv`, braucht nur `game_id`).
|
||||
- `game_alt_titles.title_search` ist auch die verbindliche Quelle für alternative Titel in der Ähnlichkeitsprüfung von #13 (`find_similar_catalog_games`, 9.5).
|
||||
|
||||
**RLS in Klartext**
|
||||
|
||||
|
|
@ -153,25 +182,25 @@ Hinweise: Das Anlegen einer gespeicherten Spalte schreibt `games` einmal neu (un
|
|||
|
||||
**5. `manual_fields`-Schlüssel ergänzen**
|
||||
|
||||
`alter table public.games drop constraint games_manual_fields_check, add constraint games_manual_fields_check check (manual_fields <@ array[…]::text[])` mit der **vollständigen** Liste aus 2a plus `'alt_titles'`.
|
||||
`alter table public.games drop constraint games_manual_fields_check, add constraint games_manual_fields_check check (manual_fields <@ array[…]::text[])` mit der **vollständigen** Liste von `main` plus `'alt_titles'`.
|
||||
|
||||
**Regel für alle Stories, die Schlüssel ergänzen (#3, #12, 2b, Import):** Jede Migration nennt die vollständige Liste aller bis dahin bekannten Schlüssel. Wer später merged, gleicht die Liste mit `main` ab (sonst entfernt die spätere Migration einen Schlüssel, und bestehende Zeilen verletzen den Check).
|
||||
**Regel für alle Stories, die Schlüssel ergänzen (#3, #12, 2b, 2c, Import):** Jede Migration nennt die vollständige Liste aller bis dahin bekannten Schlüssel. Wer später merged, gleicht die Liste mit `main` ab (sonst entfernt die spätere Migration einen Schlüssel, und bestehende Zeilen verletzen den Check).
|
||||
|
||||
**6. RPC `public.search_catalog_games`** – Vertrag in § 4.2.
|
||||
|
||||
### 3.3 Reihenfolge zu 2b und 2c (Spec #2)
|
||||
|
||||
**Merge-Reihenfolge: 2b → 2c → 3a.** 2b (`admin_merge_games`, `repoint_game_references`, Branch `feature/2b-katalog-zusammenfuehren`) und 2c (`game_reports`, Erweiterung des Fremdschlüssel-Wächters) sind in Arbeit; 3a wird danach gemergt. Folgen für 3a:
|
||||
**3a wird nach 2b gemergt.** 2b (`admin_merge_games`, `repoint_game_references`, Fremdschlüssel-Wächter in `07_catalog_merge.test.sql`, Branch `feature/2b-katalog-zusammenfuehren`) ist in Arbeit; 2c (`game_reports`) existiert noch nicht auf `origin`. Ob 2c vor oder nach 3a gemergt wird, ist offen. **Wer von 2c und 3a später merged, gleicht ab:**
|
||||
|
||||
- **Migration:** Zeitstempel von `<ts>_catalog_search.sql` liegt nach den Migrationen von 2b und 2c. B1 erstellt bzw. aktualisiert den Branch von `main` nach dem Merge von 2c (bei früherem Start: vor dem PR auf `main` aktualisieren und den Zeitstempel bei Bedarf neu vergeben, solange 3a nicht gemergt ist).
|
||||
- **`manual_fields`-Check:** vollständige Liste von `main` übernehmen, `alt_titles` ergänzen (3.2 Nr. 5).
|
||||
- **Fremdschlüssel-Wächter in `07_catalog_merge.test.sql`:** Die neue FK `game_alt_titles.game_id` macht den Wächter rot. 3a ergänzt sie im `set_eq` in der Gruppe **„bewusst ausgelassen: bleiben am ausgeblendeten Quell-Eintrag“** (neben `game_genres`, `game_mechanics`, `game_categories`, `game_publishers`), gemäß Spec-F6 A. `admin_merge_games`/`repoint_game_references` bleiben unverändert. Die Einträge von 2c (`game_reports.target_game_id` behandelt, `game_reports.provisional_game_id` ausgelassen) bleiben erhalten.
|
||||
- **`00_schema.test.sql`:** Tabellenzahl von `main` (nach 2c) + 1.
|
||||
- **pgTAP-Nummer:** `09_catalog_search.test.sql` (07 = 2b, 08 = 2c).
|
||||
- **Fremdschlüssel-Wächter in `07_catalog_merge.test.sql`:** 3a trägt `('game_alt_titles.game_id')` im `set_eq` in die Gruppe **„bewusst ausgelassen: bleiben am ausgeblendeten Quell-Eintrag“** ein (neben `game_genres`, `game_mechanics`, `game_categories`, `game_publishers`), gemäß Spec-F6 A. 2c ergänzt `game_reports.target_game_id` (behandelt) und `game_reports.provisional_game_id` (ausgelassen). Der spätere PR übernimmt die Einträge des früheren. `admin_merge_games`/`repoint_game_references` bleiben für 3a unverändert.
|
||||
- **`00_schema.test.sql`:** Tabellenzahl von `main` beim Merge + 1 (3a: `game_alt_titles`; 2c: `game_reports`).
|
||||
- **`manual_fields`-Check:** vollständige Liste von `main` übernehmen (3.2 Nr. 5).
|
||||
- **Migration:** Zeitstempel von `<ts>_catalog_search.sql` liegt nach den Migrationen, die beim Merge auf `main` sind; vor dem PR auf `main` aktualisieren und den Zeitstempel bei Bedarf neu vergeben, solange 3a nicht gemergt ist.
|
||||
- **pgTAP-Nummern:** `07` = 2b, `08` = 2c, `09` = #3 (`09_catalog_search.test.sql`), `10` = #13.
|
||||
|
||||
### 3.4 Doku zum Datenmodell
|
||||
|
||||
`docs/datenmodell-r1.md`: neue Tabelle `game_alt_titles`, generierte Spalten, Funktion `search_normalize`, RPC `search_catalog_games`, Erweiterungen, neuer `manual_fields`-Schlüssel; Abschnitt „API für das Frontend“ um die RPC ergänzen. `supabase/README.md`: Tabellenzahl, Hinweis „erste RPC mit `execute` für `anon`“. `docs/katalog-pflege.md`: Abschnitt „Alternative Titel pflegen“ (Studio-SQL und CSV-Spalte `alt_titles`).
|
||||
`docs/datenmodell-r1.md`: neue Tabelle `game_alt_titles`, generierte Spalten, Funktion `search_normalize` (mit dem gemeinsamen Satz aus 3.2 Nr. 2), RPC `search_catalog_games`, Erweiterungen, neuer `manual_fields`-Schlüssel; Abschnitt „API für das Frontend“ um die RPC ergänzen. `supabase/README.md`: Tabellenzahl, Hinweis „`search_normalize` und `search_catalog_games` sind für API-Rollen ausführbar“. `docs/katalog-pflege.md`: Abschnitte „Alternative Titel pflegen“ (Studio-SQL und CSV-Spalte `alt_titles`), „Nach einem Image-Upgrade: Normalisierung prüfen“ (R6) und im Abschnitt Zusammenführen der Schritt aus 9.5.
|
||||
|
||||
## 4. Backend
|
||||
|
||||
|
|
@ -180,7 +209,7 @@ Hinweise: Das Anlegen einer gespeicherten Spalte schreibt `games` einmal neu (un
|
|||
| Objekt | Art | PR | Aufrufbar von |
|
||||
|---|---|---|---|
|
||||
| `pg_trgm`, `unaccent` | Erweiterungen (Schema `extensions`) | 3a | – |
|
||||
| `search_normalize` | interne Funktion | 3a | nur Eigentümer (generierte Spalten, RPC) |
|
||||
| `search_normalize` | Basisfunktion | 3a | `anon`, `authenticated`, `service_role` (generierte Spalten, RPC, #13) |
|
||||
| `game_alt_titles` | Tabelle | 3a | Lesen: alle; Schreiben: Studio |
|
||||
| `title_search`, `name_search` | generierte Spalten | 3a | Lesen über bestehende Rechte |
|
||||
| `search_catalog_games` | RPC | 3a | `anon`, `authenticated` |
|
||||
|
|
@ -190,20 +219,20 @@ Keine Edge Function.
|
|||
|
||||
### 4.2 RPC `public.search_catalog_games` (Vertrag zwischen 3a und 3b)
|
||||
|
||||
`language plpgsql`, `stable`, `security invoker`, `set search_path = ''`. Alle Objekte schema-qualifiziert (`public.…`, `extensions.similarity(…)`). Aufruf über PostgREST `POST /rest/v1/rpc/search_catalog_games` (in Dart `client.rpc('search_catalog_games', params: {...})`).
|
||||
`language plpgsql`, `stable`, `security invoker`, `set search_path = ''`. Alle Objekte schema-qualifiziert (`public.…`, `extensions.similarity(…)`). Aufruf über PostgREST `POST /rest/v1/rpc/search_catalog_games` (in Dart `client.rpc('search_catalog_games', params: {...})`). Kommentar in der Funktion: Der Partial-Index `games_title_search_trgm_idx` ist nur über die Bedingungen der View nutzbar.
|
||||
|
||||
**Eingabe** (alle optional, `null` = nicht gesetzt)
|
||||
|
||||
| Parameter | Typ | Zulässig | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `p_query` | text | ≤ 100 Zeichen | Suchbegriff. `null` oder nur Leerzeichen = kein Suchbegriff. Ergibt die Normalisierung `null` (z. B. `'!!!'`), gibt es **keine Treffer**. |
|
||||
| `p_query` | text | ≤ 100 Zeichen, ≤ 10 Wörter nach Normalisierung | Suchbegriff. `null` oder nur Leerzeichen = kein Suchbegriff. Ergibt die Normalisierung `null` (z. B. `'!!!'`), gibt es **keine Treffer**. Hat der normalisierte Begriff weniger als 2 Zeichen, gilt er als **kein Suchbegriff**. |
|
||||
| `p_players` | smallint | 1–99 | Spielerzahl n: passt, wenn `min_players <= n <= max_players` (beide bekannt) |
|
||||
| `p_max_minutes` | smallint | 1–1440 | „höchstens X Minuten“: passt, wenn `coalesce(max_minutes, min_minutes) <= X` (mindestens einer bekannt) |
|
||||
| `p_max_minutes` | smallint | 1–1440 | „höchstens X Minuten“: passt, wenn `coalesce(max_minutes, min_minutes) <= X`; es genügt eine der beiden Angaben (nur `max_minutes` oder nur `min_minutes` bekannt zählt) |
|
||||
| `p_age` | smallint | 0–99 | „für Spielende ab X Jahren geeignet“: passt, wenn `min_age <= X` (bekannt) |
|
||||
| `p_genre_ids` | uuid[] | ≤ 20 Einträge | Spiel hat **jedes** dieser Genres (Spec-F1; Duplikate werden entfernt; unbekannte IDs ergeben 0 Treffer) |
|
||||
| `p_mechanic_ids` | uuid[] | ≤ 20 Einträge | wie Genres |
|
||||
| `p_limit` | integer | 1–50, Standard 30 | Seitengröße |
|
||||
| `p_offset` | integer | 0–10 000, Standard 0 | Versatz |
|
||||
| `p_offset` | integer | 0–10 000, Standard 0 | Versatz. Die App lädt nicht über 10 000 hinaus nach (`hasMore` wird dort `false`). |
|
||||
|
||||
Die Semantik von Spielerzahl, Dauer und Alter steht unter Vorbehalt von Spec-F2.
|
||||
|
||||
|
|
@ -215,7 +244,7 @@ Die Semantik von Spielerzahl, Dauer und Alter steht unter Vorbehalt von Spec-F2.
|
|||
| `title` | text | Haupttitel |
|
||||
| `year` | smallint | nullable |
|
||||
| `min_players`, `max_players`, `min_minutes`, `max_minutes`, `min_age` | smallint | nullable |
|
||||
| `image_path` | text | aus `catalog_games` (maskiert nach Spec #2 E6); im Startbestand immer `null` |
|
||||
| `image_path` | text | **nur aus `catalog_games`** (maskiert nach Spec #2 E6), nie aus `games`; im Startbestand immer `null`. Ein Speicherpfad, keine URL; die Umwandlung in eine URL kommt mit der Dateizugriffsschicht (Storage), in #3 bleibt `GameCover.imageUrl` immer `null`. |
|
||||
| `publisher_names` | text[] | alle Verlage, sortiert nach `name_search`, leer statt `null` |
|
||||
| `matched_alt_title` | text | der alternative Titel, über den das Spiel besser getroffen wurde als über den Haupttitel; sonst `null` (Anzeige „auch: …“) |
|
||||
| `match_rank` | smallint | Relevanzstufe 0–4 (unten), `null` ohne Suchbegriff; für Tests und Fehlersuche, die App zeigt sie nicht an |
|
||||
|
|
@ -223,10 +252,12 @@ Die Semantik von Spielerzahl, Dauer und Alter steht unter Vorbehalt von Spec-F2.
|
|||
|
||||
Beschreibung, Genres und Mechaniken sind nicht enthalten (Listenansicht, kleine Antwort). Die Detailseite (#4) liest sie selbst.
|
||||
|
||||
**Treffer-Regel** (E4), mit `q := search_normalize(p_query)`, Wörter `w := string_to_array(q, ' ')` (höchstens die ersten 10), `qc := replace(q, ' ', '')`:
|
||||
**Treffer-Regel** (E4), mit `q := search_normalize(p_query)`, `qc := replace(q, ' ', '')` und den Wörtern `w :=` alle Wörter aus `string_to_array(q, ' ')` **mit mindestens 2 Zeichen** (Wörter mit 1 Zeichen werden verworfen):
|
||||
|
||||
- `char_length(q) < 2` → kein Suchbegriff (wie `null`).
|
||||
- Mehr als 10 Wörter in `q` → Fehler `search_query_too_long` (kein stilles Abschneiden).
|
||||
- Kandidaten-Titel eines Spiels: `games.title_search` und alle `game_alt_titles.title_search`.
|
||||
- Ein Spiel trifft, wenn **jedes** Wort aus `w` als Teilzeichenkette (`like '%' || wort || '%'`; Wörter enthalten nur `a–z0–9`, ein Escapen entfällt) in einem Kandidaten-Titel **oder** in einem `publishers.name_search` des Spiels vorkommt, **oder** wenn `qc` Teilzeichenkette eines Kandidaten-Titels ohne Leerzeichen ist.
|
||||
- Ein Spiel trifft, wenn **jedes** Wort aus `w` als Teilzeichenkette (`like '%' || wort || '%'`; Wörter enthalten nur `a–z0–9`, ein Escapen entfällt) in einem Kandidaten-Titel **oder** in einem `publishers.name_search` des Spiels vorkommt, **oder** wenn `qc` Teilzeichenkette eines Kandidaten-Titels ohne Leerzeichen ist. Ist `w` nach dem Verwerfen leer (z. B. „a b“), trifft nur die Regel mit `qc`.
|
||||
- Ein Spiel erscheint immer genau einmal, auch wenn mehrere Titel treffen.
|
||||
|
||||
**Relevanz** (E5): `match_rank` ist die beste Stufe über alle Kandidaten-Titel.
|
||||
|
|
@ -247,29 +278,33 @@ Sortierung: `match_rank` aufsteigend, dann `extensions.similarity(title_search,
|
|||
|
||||
| Fall | Meldung |
|
||||
|---|---|
|
||||
| `p_query` länger als 100 Zeichen | `search_query_too_long` |
|
||||
| `p_query` länger als 100 Zeichen oder mehr als 10 Wörter | `search_query_too_long` |
|
||||
| `p_players`, `p_max_minutes` oder `p_age` außerhalb des Bereichs; mehr als 20 IDs in einem Array | `search_filter_invalid` |
|
||||
| `p_limit` oder `p_offset` außerhalb des Bereichs | `search_paging_invalid` |
|
||||
|
||||
Die App verhindert diese Fälle schon bei der Eingabe; tritt einer trotzdem auf, ist das ein Programmfehler (Log, allgemeiner Fehlerzustand).
|
||||
Die App verhindert diese Fälle schon bei der Eingabe (Feld begrenzt auf 100 Zeichen; mehr als 10 Wörter sind in der Praxis nicht zu erwarten und landen als Programmfehler im allgemeinen Fehlerzustand); tritt einer trotzdem auf, wird er geloggt (ohne Suchbegriff).
|
||||
|
||||
**Sicherheit:** `security invoker` – für `anon` gelten die Policies von `games`, `catalog_sources`, `game_publishers`, `publishers`, `game_alt_titles`, `game_genres`, `game_mechanics`. Die Eingabegrenzen begrenzen die Last je Aufruf; zusätzlich greift das `statement_timeout` der Rollen `anon`/`authenticated` (Supabase-Standard; B5 prüft den Wert im Self-Hosting, Ergebnis in `docs/katalog-pflege.md` bzw. `supabase/README.md`). Eine Mengenbegrenzung je Client gibt es in R1 nicht (R4).
|
||||
**Sicherheit:** `security invoker` – für `anon` gelten die Policies von `games`, `catalog_sources`, `game_publishers`, `publishers`, `game_alt_titles`, `game_genres`, `game_mechanics`. Die Eingabegrenzen begrenzen die Last je Aufruf. Zusätzlich gilt das `statement_timeout` der Rollen (lokal: `anon` 3 s, `authenticated` 8 s; B5 prüft die Werte im Self-Hosting). Ein `set statement_timeout` an der Funktion selbst wirkt **nicht** (die Einstellung gilt nur für die Anweisung, die vor dem Funktionsaufruf startet) und wird deshalb nicht verwendet. Das eigentliche Risiko sind viele parallele Aufrufe ohne Anmeldung (R4): Eine Mengenbegrenzung je IP (Kong-Plugin `rate-limiting` auf `/rest/v1/rpc/search_catalog_games` oder `limit_req` im vorgelagerten Proxy) wird **vor dem Go-live** in D1 bzw. im Hosting-Runbook entschieden; für R1.1 ist sie kein Muss.
|
||||
|
||||
**Leistung (E12):** Ziel für die Ausführung in der Datenbank: p95 ≤ 150 ms bei 10 000 sichtbaren Katalogspielen, 20 000 alternativen Titeln, 2 000 Verlagen und je Spiel zwei Genres und drei Mechaniken. B5 misst lokal mit synthetischen Daten (Skript `supabase/tests/perf/catalog_search_perf.sql`, nicht Teil von `supabase test db`, läuft nie gegen Staging/Produktion) und dokumentiert `explain (analyze, buffers)` für typische Fälle im PR. Wird das Ziel verfehlt: Rückfall in R2.
|
||||
**Leistung (E12):** Ziel für die Ausführung in der Datenbank: p95 ≤ 150 ms bei 10 000 sichtbaren Katalogspielen, 20 000 alternativen Titeln, 2 000 Verlagen und je Spiel zwei Genres und drei Mechaniken. B5 misst **als `anon` mit RLS** (`set local role anon`) lokal mit synthetischen Daten (Skript `supabase/tests/perf/catalog_search_perf.sql`, nicht Teil von `supabase test db`, läuft nie gegen Staging/Produktion) und dokumentiert `explain (analyze, buffers)` für typische Fälle im PR (Titelteil, 2-Zeichen-Begriff, Begriff ohne Leerzeichen, Verlag, nur Filter, ohne alles). Wird das Ziel verfehlt, in dieser Reihenfolge:
|
||||
|
||||
1. **Kandidaten-CTE:** Vorauswahl der Spiel-IDs per `union` über die GIN-Indizes (nur Wörter ≥ 3 Zeichen, sonst ohne Vorauswahl), danach Treffer-Regel und Relevanz nur auf den Kandidaten.
|
||||
2. **Generierte Spalte `title_compact`** (`replace(search_normalize(title), ' ', '')`) an `games` und `game_alt_titles` mit Trigramm-Index, damit die Regel mit `qc` indexierbar wird.
|
||||
3. Rückfall R2 (zusammengefasster Suchtext je Spiel).
|
||||
|
||||
### 4.3 Startbestand-Werkzeug: Spalte `alt_titles` auswerten
|
||||
|
||||
Erweiterung von `supabase/catalog/build_startbestand.ts` (Spec #2 4.3, E9). Die Spalte ist schon reserviert.
|
||||
|
||||
- **Format:** `de:Titel;en:Title`. Einträge durch `;` getrennt; je Eintrag Sprache und Titel am **ersten** `:` getrennt (Titel dürfen `:` enthalten, z. B. `de:Catan: Das Duell`). Leerzeichen am Rand werden entfernt.
|
||||
- **Prüfungen** (Abbruch mit Zeilennummer und Grund): Sprache nicht `de`/`en`; leerer Titel; Titel länger als 200 Zeichen; derselbe Titel in derselben Sprache doppelt; alternativer Titel gleich dem Haupttitel. „Gleich“ heißt im Skript: Kleinbuchstaben, diakritische Zeichen entfernt (Unicode NFD), alles außer `a–z0–9` zu Leerzeichen – eine Annäherung an `search_normalize`; maßgeblich bleibt die Unique-Constraint in der Datenbank.
|
||||
- **Format:** `de:Titel;en:Title`. Einträge durch `;` getrennt; je Eintrag Sprache und Titel am **ersten** `:` getrennt (Titel dürfen `:` enthalten, z. B. `de:Catan: Das Duell`). Leerzeichen am Rand werden entfernt. **`;` ist in alternativen Titeln nicht erlaubt** (kein Escaping); betroffene Titel pflegt Chris im Studio.
|
||||
- **Prüfungen** (Abbruch mit Zeilennummer und Grund): Sprache nicht `de`/`en`; Eintrag ohne `:`; leerer Titel; Titel länger als 200 Zeichen; derselbe Titel in derselben Sprache doppelt; alternativer Titel gleich dem Haupttitel. „Gleich“ heißt im Skript: Marken- und Schutzzeichen entfernt, Kleinbuchstaben, diakritische Zeichen entfernt (Unicode NFD), alles außer `a–z0–9` zu Leerzeichen – eine Annäherung an `search_normalize`; maßgeblich bleibt die Unique-Constraint in der Datenbank.
|
||||
- **Erzeugte SQL je Startbestandsspiel** (Abgleich wie bei den Zuordnungen, Spec #2 4.3 Idempotenz Regel 3):
|
||||
1. `delete from public.game_alt_titles a where a.game_id = <id> and (a.locale, a.title_search) not in (select locale, public.search_normalize(title) from (values …) v(locale, title))`,
|
||||
2. `insert into public.game_alt_titles (game_id, locale, title) values … on conflict (game_id, locale, title_search) do update set title = excluded.title where game_alt_titles.title is distinct from excluded.title`.
|
||||
Unveränderte Zeilen bleiben unberührt (`updated_at` und `manual_fields` stabil). Spiele ohne `alt_titles` bekommen nur Schritt 1 (entfernt also früher gesetzte Titel).
|
||||
- Alternative Titel bekommen **keine** abgeleitete ID (wie Verlage); Konfliktziel ist die Unique-Constraint.
|
||||
- `checks.sql`: keine neue Pflichtregel (alternative Titel sind optional); eine Informationszeile „Startbestandsspiele mit alternativen Titeln: n“.
|
||||
- Die erzeugten Dateien (`startbestand.sql`, Testdaten unter `supabase/catalog/testdata/`, `supabase/tests/support/*.inc`) werden neu erzeugt; der CI-Schritt „erzeugte Dateien aktuell“ verlangt das. Die Testdaten-CSV bekommt bei einem Testspiel zwei alternative Titel (de und en).
|
||||
- Die erzeugten Dateien (`startbestand.sql`, Testdaten unter `supabase/catalog/testdata/`, `supabase/tests/support/*.inc`) werden neu erzeugt; der CI-Schritt „erzeugte Dateien aktuell“ verlangt das. Die Testdaten-CSV bekommt bei **Testspiel B** zwei alternative Titel (de und en), nicht bei Testspiel A (§ 7, Auswirkung auf `06`).
|
||||
- **Bestehender Deno-Test bricht absichtlich:** `"facts_source, alt_titles und base_game_key landen nicht in der SQL"` (`build_startbestand_test.ts`, Z. 253) wird zu „`facts_source` und `base_game_key` landen nicht in der SQL“ geändert; neue Tests für `alt_titles` (§ 7.1).
|
||||
- Das Einspielen markiert `alt_titles` in `manual_fields` (Trigger, Spec #2 SB8). Startbestand ab Go-live eingefroren (Spec #2 Spec-F6): Pflegt Chris alternative Titel vorher, kommen sie über die CSV; danach nur über Studio.
|
||||
|
||||
|
|
@ -299,12 +334,12 @@ app/lib/features/catalog/
|
|||
search/widgets/… Suchfeld, Filterbereich, Treffer-Kachel, Listenfuß, Leerzustand
|
||||
```
|
||||
|
||||
Domäne ohne Flutter- und Supabase-Importe (`freezed_annotation` ist erlaubt). Datenklassen **ohne `toJson`** (E9); das Feature schreibt nichts in die Datenbank.
|
||||
Domäne ohne Flutter- und Supabase-Importe (`freezed_annotation` ist erlaubt), Domänenmodelle ohne JSON. **DTOs nur mit `json_serializable`** (`createToJson: false`, kein freezed-JSON); das Feature schreibt nichts in die Datenbank (E9).
|
||||
|
||||
### 5.2 Domänenmodelle und Repository
|
||||
|
||||
- **`CatalogGameSummary`:** `id`, `title`, `year?`, `minPlayers?`, `maxPlayers?`, `minMinutes?`, `maxMinutes?`, `minAge?`, `imagePath?`, `publisherNames` (`List<String>`, nie `null`), `matchedAltTitle?`. **Abweichung von Spec #2 § 5:** Dort heißt es, KAT-01 lege `CatalogGame` an. Die Suche braucht nur die Listenansicht; das vollständige Modell `CatalogGame` (mit Beschreibung, Genres, Quellenangabe) legt KAT-02 (#4) an.
|
||||
- **`CatalogSearchQuery`:** `text` (getrimmt, ≤ 100 Zeichen), `filters` (`players?`, `maxMinutes?`, `minAgeSuitable?`, `genreIds` `Set<String>`, `mechanicIds` `Set<String>`). Hilfen: `hasText` (≥ 2 Zeichen nach Trim), `hasFilters`, `activeFilterCount`, `isEmpty`. Zulässige Werte als Konstanten (Spielerzahl 1–12, Dauer-Stufen 30/45/60/90/120/180, Alters-Stufen 6/8/10/12/14/16/18; Spec-F2).
|
||||
- **`CatalogGameSummary`:** `id`, `title`, `year?`, `minPlayers?`, `maxPlayers?`, `minMinutes?`, `maxMinutes?`, `minAge?`, `imagePath?`, `publisherNames` (`List<String>`, nie `null`), `matchedAltTitle?`. `imagePath` ist ein Speicherpfad aus `catalog_games`; in #3 wird daraus keine URL erzeugt (`GameCover.imageUrl` bleibt `null`, Platzhalter). **Abweichung von Spec #2 § 5:** Dort heißt es, KAT-01 lege `CatalogGame` an. Die Suche braucht nur die Listenansicht; das vollständige Modell `CatalogGame` (mit Beschreibung, Genres, Quellenangabe) legt KAT-02 (#4) an.
|
||||
- **`CatalogSearchQuery`:** `text` (getrimmt, ≤ 100 Zeichen), `filters` (`players?`, `maxMinutes?`, `minAgeSuitable?`, `genreIds` `Set<String>`, `mechanicIds` `Set<String>`). Hilfen: `hasText` (≥ 2 Zeichen nach Trim), `hasFilters`, `activeFilterCount`, `isEmpty`. Zulässige Werte als Konstanten (Spielerzahl 1–12, Dauer-Stufen 30/45/60/90/120/180, Alters-Stufen 6/8/10/12/14/16/18; Spec-F2; höchstens 20 Genres bzw. Mechaniken).
|
||||
- **`CatalogTerm`:** Genre bzw. Mechanik; `localizedName('de')` → `nameDe`, sonst `nameEn` (QUE-05 Krit. 4: Englisch als Rückfall).
|
||||
- **`CatalogFailure`:** `network` (keine Verbindung, Zeitüberschreitung), `invalidInput` (P0001 aus § 4.2, Programmfehler), `unexpected`.
|
||||
- **`CatalogRepository`:**
|
||||
|
|
@ -320,7 +355,7 @@ Domäne ohne Flutter- und Supabase-Importe (`freezed_annotation` ist erlaubt). D
|
|||
|
||||
### 5.4 Zustand und Verhalten
|
||||
|
||||
**`CatalogSearchState`** (freezed): `query`, `status` (`loading` | `data` | `empty` | `error`), `items`, `totalCount`, `failure?`, `refreshing` (neue Suche läuft, alte Treffer bleiben sichtbar), `loadingMore`, `loadMoreFailure?`. Abgeleitet: `hasMore = items.length < totalCount`.
|
||||
**`CatalogSearchState`** (freezed): `query`, `status` (`loading` | `data` | `empty` | `error`), `items`, `totalCount`, `failure?`, `refreshing` (neue Suche läuft, alte Treffer bleiben sichtbar), `loadingMore`, `loadMoreFailure?`. Abgeleitet: `hasMore = items.length < totalCount && items.length < 10000`.
|
||||
|
||||
**Methoden des Controllers**
|
||||
|
||||
|
|
@ -340,7 +375,7 @@ Domäne ohne Flutter- und Supabase-Importe (`freezed_annotation` ist erlaubt). D
|
|||
|
||||
- **`AppRoutes.catalog = '/catalog'`**, öffentlich (nicht in `privateRoutePaths`). Registrierung über `catalogRoutes` in `app_router.dart` (Muster `authRoutes`).
|
||||
- **Query-Parameter:** `q` (Suchbegriff), `players`, `maxMinutes`, `age`, `genre` und `mechanic` (IDs, durch Komma getrennt). Ungültige oder unbekannte Werte werden still ignoriert (geparst in `presentation`, geprüft gegen die Konstanten der Domäne).
|
||||
- **URL aktualisieren (Web):** Nach jeder ausgeführten Suche wird die URL ohne neuen Verlaufseintrag ersetzt, sodass Neuladen und Teilen den Stand erhalten. flutter-dev prüft mit go_router 18, dass dabei weder Seite noch Controller neu entstehen (Zustand, Scrollposition, Fokus im Suchfeld). **Falls doch:** Das Schreiben der URL entfällt, nur das Lesen beim Öffnen bleibt; Abweichung im PR begründen.
|
||||
- **URL aktualisieren (Web):** Nach jeder ausgeführten Suche wird die URL ohne neuen Verlaufseintrag und ohne neue Navigation ersetzt, sodass Neuladen und Teilen den Stand erhalten. Eingeplant ist `Router.neglect` zusammen mit `SystemNavigator.routeInformationUpdated(uri: …, replace: true)` statt eines `context.go`/`replace`, damit weder Seite noch Controller neu entstehen (Zustand, Scrollposition, Fokus im Suchfeld). flutter-dev prüft das mit go_router 18. **Falls es nicht zuverlässig funktioniert:** Das Schreiben der URL entfällt, nur das Lesen beim Öffnen bleibt; Abweichung im PR begründen.
|
||||
- **Startseite:** Der bestehende Platzhalter `HomeScreen` bekommt eine Schaltfläche „Spiele suchen“ → `/catalog`. Die Startseite ohne Anmeldung (Suche direkt auf `/`) baut #29 (QUE-09 Krit. 13); dafür ist der Inhalt des Screens als eigenes Widget `CatalogSearchView` gekapselt.
|
||||
- **Antippen eines Treffers:** Die Kachel hat einen `onTap`-Parameter; in #3 ist er `null` (Kachel nicht interaktiv, kein toter Link). KAT-02 (#4) verdrahtet die Navigation zur Detailseite.
|
||||
|
||||
|
|
@ -353,9 +388,9 @@ Aufbau (Details und Texte liefert ui-designer in U1, Komponenten ins Design-Syst
|
|||
- kompakt (< 600 dp) und mittel (600–1023 dp): Schaltfläche „Filter“ mit Anzahl aktiver Filter; öffnet ein Bottom Sheet. Änderungen wirken sofort (Liste dahinter aktualisiert sich), „Fertig“ schließt.
|
||||
- groß (≥ 1024 dp, `AppWidthClass` aus `app_spacing.dart`): feste Seitenleiste links (ca. 320 dp), Trefferliste rechts mit begrenzter Breite (Plattformen Krit. 2).
|
||||
- Inhalte: Spielerzahl (Auswahl „beliebig“, 1–12), Spieldauer (Einfachauswahl „bis 30/45/60/90/120/180 Min.“), Alter (Einfachauswahl „ab 6 … ab 18 Jahren geeignet“), Genre und Mechanik (Mehrfachauswahl als Filter-Chips, nach lokalisiertem Namen sortiert), „Filter zurücksetzen“. Die genaue Beschriftung von Alter und Dauer folgt Spec-F2.
|
||||
- **Trefferzahl** über der Liste („23 Spiele“, ARB-Plural) und **Ansage** für Screenreader nach jeder abgeschlossenen Suche (Live-Region bzw. aktuelle Flutter-API für Ansagen).
|
||||
- **Treffer-Kachel:** `GameCover` klein mit `titleVisibleNearby: true` (Platzhalter, KAT-05 Krit. 10), Titel (max. 2 Zeilen), Jahr, Verlage (bis zu zwei, sonst „+n“), Kurzangaben Spielerzahl („2–4 Spieler“), Dauer („30–60 Min.“), Alter („ab 10“); fehlende Angaben werden weggelassen. Bei `matchedAltTitle` eine Zeile „auch: {Titel}“. Ein zusammengefasstes Semantics-Label je Kachel.
|
||||
- **Listenfuß:** während des Nachladens eine Ladeanzeige; bei `hasMore` eine Schaltfläche „Weitere laden“; bei Fehler beim Nachladen Hinweis + „Erneut versuchen“ (bereits geladene Treffer bleiben). Automatisches Nachladen, sobald die letzten 5 Kacheln in Sicht kommen.
|
||||
- **Trefferzahl** über der Liste („23 Spiele“, ARB-Plural) und **Ansage** für Screenreader: über `SemanticsService.sendAnnouncement` bzw. eine Live-Region, **nur nach einer abgeschlossenen Suche** (nicht während des Tippens, nicht bei verworfenen Antworten) und entprellt, damit schnelle Filterwechsel keine Ansagenkette erzeugen.
|
||||
- **Treffer-Kachel:** `GameCover` klein mit `titleVisibleNearby: true` und `imageUrl: null` (Platzhalter, KAT-05 Krit. 10), Titel (max. 2 Zeilen), Jahr, Verlage (bis zu zwei, sonst „+n“), Kurzangaben Spielerzahl („2–4 Spieler“), Dauer („30–60 Min.“), Alter („ab 10“); fehlende Angaben werden weggelassen. Bei `matchedAltTitle` eine Zeile „auch: {Titel}“. Ein zusammengefasstes Semantics-Label je Kachel.
|
||||
- **Listenfuß:** während des Nachladens eine Ladeanzeige; bei `hasMore` eine Schaltfläche „Weitere laden“; bei Fehler beim Nachladen Hinweis + „Erneut versuchen“ (bereits geladene Treffer bleiben). Automatisches Nachladen, sobald die letzten 5 Kacheln in Sicht kommen. **Fokus:** Nach „Weitere laden“ per Tastatur oder Screenreader springt der Fokus auf die erste neu geladene Kachel (nicht zurück an den Listenanfang).
|
||||
|
||||
**Zustände** (Zustände Krit. 1, 2)
|
||||
|
||||
|
|
@ -366,7 +401,7 @@ Aufbau (Details und Texte liefert ui-designer in U1, Komponenten ins Design-Syst
|
|||
| Daten | Trefferzahl + Liste |
|
||||
| Leer mit Suchbegriff (Krit. 6) | „Keine Spiele zu „{q}“ gefunden.“ + Hinweis „Prüfe die Schreibweise oder entferne Filter.“; „Filter zurücksetzen“ (nur wenn Filter aktiv); **Einstiegspunkt „Spiel melden“**, sichtbar erst mit #13 (E11, Spec-F4) |
|
||||
| Leer nur mit Filtern (Krit. 8) | „Keine Spiele passen zu deinen Filtern.“ + „Filter zurücksetzen“ |
|
||||
| Leer ohne Suchbegriff und Filter | „Der Katalog ist noch leer.“ (nur lokal/Staging ohne Startbestand denkbar) |
|
||||
| Leer ohne Suchbegriff und Filter | „Der Katalog ist noch leer.“ (nur lokal ohne Startbestand denkbar) |
|
||||
| Fehler: keine Verbindung | „Keine Verbindung. Für die Suche brauchst du Internet.“ + „Erneut versuchen“ |
|
||||
| Fehler: sonstige | „Die Suche ist gerade nicht möglich.“ + „Erneut versuchen“ |
|
||||
|
||||
|
|
@ -388,11 +423,11 @@ Der Einstiegspunkt für #13 ist ein optionaler Callback am Leerzustand-Widget (`
|
|||
|
||||
Werkzeuge: pgTAP (`supabase test db`), Deno-Tests (Startbestand-Werkzeug), `flutter_test` + `mocktail` (Unit/Widget), Playwright (Web-E2E und Messung). Testnamen enthalten die Referenz („KAT-01 Kriterium n“).
|
||||
|
||||
**pgTAP-Datei `supabase/tests/database/09_catalog_search.test.sql`** (3a). Die Nummern `07` und `08` sind für Spec #2 (2b, 2c) reserviert. Die Tests laufen mit eingespieltem Startbestand: Fixtures mit festen Test-UUIDs und einem eindeutigen Kunstwort in allen Titeln und Verlagen (z. B. `qxz`), sodass Suchanfragen nie Startbestandsspiele treffen; bei Abfragen ohne Suchbegriff wird nur auf die Fixture-IDs geprüft.
|
||||
**pgTAP-Datei `supabase/tests/database/09_catalog_search.test.sql`** (3a; `07` = 2b, `08` = 2c, `10` = #13). Die Tests laufen mit eingespieltem Startbestand: Fixtures mit festen Test-UUIDs und einem eindeutigen Kunstwort in allen Titeln und Verlagen (z. B. `qxz`), sodass Suchanfragen nie Startbestandsspiele treffen; bei Abfragen ohne Suchbegriff wird nur auf die Fixture-IDs geprüft.
|
||||
|
||||
**Auswirkung auf bestehende Tests** (geprüft gegen `main` und `feature/2b-katalog-zusammenfuehren`):
|
||||
|
||||
- **`00_schema.test.sql`:** nur die Tabellenzahl (+ 1 gegenüber dem Stand von `main` beim Merge von 3a, also nach 2b und 2c, siehe 3.3). Es gibt keine `columns_are`-Prüfungen; die neuen generierten Spalten `title_search`/`name_search` brechen dort nichts.
|
||||
- **`00_schema.test.sql`:** nur die Tabellenzahl (+ 1 gegenüber dem Stand von `main` beim Merge von 3a, siehe 3.3). Es gibt keine `columns_are`-Prüfungen; die neuen generierten Spalten `title_search`/`name_search` brechen dort nichts.
|
||||
- **`06_catalog_foundation.test.sql`:** Die Spaltenrechte-Prüfungen (Z. 293–302) arbeiten mit festen Spaltenlisten und sind von den generierten Spalten nicht betroffen. **Aber Z. 532–535 prüft `manual_fields` von „Testspiel A“ exakt.** Bekäme Testspiel A alternative Titel, käme `alt_titles` hinzu und der Test würde rot. Vorgabe für B3: die alternativen Titel in `supabase/catalog/testdata/startbestand.csv` an **Testspiel B** hängen (Testspiel A bleibt unverändert); nur falls das nicht geht, passt B2 die erwartete Liste in Z. 532–535 an. Testspiel B hat bereits zwei Verlage und eignet sich auch für Krit. 3.
|
||||
- **`07_catalog_merge.test.sql`** (2b): Fremdschlüssel-Wächter um `game_alt_titles.game_id` erweitern (3.3).
|
||||
|
||||
|
|
@ -400,38 +435,42 @@ Werkzeuge: pgTAP (`supabase test db`), Deno-Tests (Startbestand-Werkzeug), `flut
|
|||
|
||||
| AK | Unit | Widget | Integration/E2E | RLS/pgTAP | Task |
|
||||
|---|---|---|---|---|---|
|
||||
| Krit. 1 | Repository: Parameter nur aus gesetzten Werten, Suchbegriff < 2 Zeichen wird nicht gesendet; DTO-Parsing (alle Felder nullable, `publisher_names` leer); Controller: Entprellung 300 ms, Enter sofort, nur letzte Antwort zählt | Suchfeld: Eingabe → Liste; Löschen-Schaltfläche; Treffer-Kachel mit Kurzangaben, fehlende Angaben weggelassen | Web: Teil eines Titels in anderer Schreibweise und mit Sonderzeichen findet das Spiel | `search_normalize`-Pflichtfälle (3.2); Teilzeichenkette, Groß/Klein, Sonderzeichen, Umlaut/ß; mehrere Wörter (UND); Schreibweise ohne Leerzeichen (`qc`); Relevanzstufen 0–4 in erwarteter Reihenfolge; gleiche Stufe → Ähnlichkeit, Titel, ID; `'!!!'` → 0 Treffer; `null`/Leerzeichen → kein Suchbegriff | B1, B2, F1, F2, T1, T2 |
|
||||
| Krit. 2 | DTO: `matched_alt_title` | Kachel zeigt „auch: …“ | Web: Suche nach deutschem und englischem alternativen Titel findet dasselbe Spiel | Treffer über `de`- und `en`-Titel; Spiel nur einmal, auch wenn Haupt- und alternativer Titel treffen; `matched_alt_title` nur bei besserer Stufe; Tabelle: Checks (Sprache, Länge, nur Sonderzeichen → 23514), Unique (Schreibweise egal → 23505); RLS: `anon` liest Titel von Katalogspielen, nicht die eines fremden privaten Spiels; `anon`/`authenticated` können nicht schreiben (42501); Einfügen als `postgres` markiert `alt_titles` in `manual_fields`; Check akzeptiert `alt_titles`, lehnt unbekannte Schlüssel ab. **Idempotenz in pgTAP** über die neu erzeugten `.inc`-Fixtures (`startbestand_fixture.inc` zweimal einspielen, wie in `06`): zweites Einspielen ändert keine Zeile von `game_alt_titles` (Vergleich ohne `updated_at`, dazu `updated_at` unverändert); ein in der CSV entfernter alternativer Titel wird gelöscht; geänderte Schreibweise aktualisiert nur `title`. Deno (nur das Skript, ohne Datenbank): Format, Sprache, Länge, Duplikat, gleich Haupttitel, `:` im Titel; erzeugte SQL (Snapshot) | B1, B2, B3, T1, T2 |
|
||||
| Krit. 1 | Repository: Parameter nur aus gesetzten Werten, Suchbegriff < 2 Zeichen wird nicht gesendet; DTO-Parsing (alle Felder nullable, `publisher_names` leer); Controller: Entprellung 300 ms, Enter sofort, nur letzte Antwort zählt | Suchfeld: Eingabe → Liste; Löschen-Schaltfläche; Treffer-Kachel mit Kurzangaben, fehlende Angaben weggelassen | Web: Teil eines Titels in anderer Schreibweise und mit Sonderzeichen findet das Spiel | `search_normalize`-Pflichtfälle (3.2 Nr. 2, inkl. `®`, `™`, `½`, `Æon’s End`); Teilzeichenkette, Groß/Klein, Sonderzeichen, Umlaut/ß; mehrere Wörter (UND); Wörter mit 1 Zeichen werden verworfen; normalisierter Begriff < 2 Zeichen = kein Suchbegriff; Schreibweise ohne Leerzeichen (`qc`, „aeons end“ findet „Æon’s End“); Relevanzstufen 0–4 in erwarteter Reihenfolge; gleiche Stufe → Ähnlichkeit, Titel, ID; `'!!!'` → 0 Treffer; `null`/Leerzeichen → kein Suchbegriff | B1, B2, F1, F2, T1, T2 |
|
||||
| Krit. 2 | DTO: `matched_alt_title` | Kachel zeigt „auch: …“ | Web: Suche nach deutschem und englischem alternativen Titel findet dasselbe Spiel | Treffer über `de`- und `en`-Titel; Spiel nur einmal, auch wenn Haupt- und alternativer Titel treffen; `matched_alt_title` nur bei besserer Stufe; Tabelle: Checks (Sprache, Länge, nur Sonderzeichen → 23514), Unique (Schreibweise egal → 23505); RLS: `anon` liest Titel von Katalogspielen, nicht die eines fremden privaten Spiels; `anon`/`authenticated` können nicht schreiben (42501); Einfügen als `postgres` markiert `alt_titles` in `manual_fields`; Check akzeptiert `alt_titles`, lehnt unbekannte Schlüssel ab. **Idempotenz in pgTAP** über die neu erzeugten `.inc`-Fixtures (`startbestand_fixture.inc` zweimal einspielen, wie in `06`): zweites Einspielen ändert keine Zeile von `game_alt_titles` (Vergleich ohne `updated_at`, dazu `updated_at` unverändert); ein in der CSV entfernter alternativer Titel wird gelöscht; geänderte Schreibweise aktualisiert nur `title`. Deno (nur das Skript, ohne Datenbank): Format, Sprache, Länge, Duplikat, gleich Haupttitel, `:` im Titel erlaubt, `;` im Titel → Fehler mit Zeilennummer; erzeugte SQL (Snapshot) | B1, B2, B3, T1, T2 |
|
||||
| Krit. 3 | – | Kachel: Verlage (bis zwei, „+n“) | Web: Verlagsname findet dessen Spiele | Verlag findet alle seine Spiele; Spiel mit zwei Verlagen über beide; Wort aus Titel + Wort aus Verlag (UND); `publisher_names` sortiert, vollständig; Stufe 4 hinter Titeltreffern | B1, B2, T2 |
|
||||
| Krit. 4 | `CatalogSearchQuery`: Grenzen, `activeFilterCount`; URL-Parameter lesen/schreiben (Rundreise, ungültige Werte ignoriert); Controller: Filteränderung startet ab Versatz 0 | Filterbereich: jede Filterart setzt den Wert; Zurücksetzen; Bottom Sheet (kompakt) und Seitenleiste (≥ 1024 dp); Begriffe nach Sprache sortiert und benannt; Fehler beim Laden der Begriffe | Web: Filter Genre + Spielerzahl mit Suchbegriff | je Filter allein; Kombination aller Filter (UND); zwei Genres → nur Spiele mit beiden (Spec-F1); fehlende Angaben werden ausgeschlossen (Spec-F2); unbekannte Genre-ID → 0 Treffer; Duplikate im Array zählen einfach | B1, B2, F1, F2, U1, T1, T2 |
|
||||
| Krit. 5 | Controller: `loadMore` hängt an, verwirft doppelte IDs, nicht doppelt gleichzeitig; Fehler beim Nachladen behält Treffer; `retry` | Listenfuß: Ladeanzeige, „Weitere laden“, Fehler + „Erneut versuchen“; automatisches Nachladen beim Scrollen | Web: ≥ 31 Fixture-Treffer, Scrollen lädt die zweite Seite; „Weitere laden“ per Tastatur | Seiten ohne Überschneidung, Vereinigung = Gesamtmenge, Reihenfolge stabil über Aufrufe; `total_count` gleich in allen Zeilen und Seiten | B2, F2, T1, T2 |
|
||||
| Krit. 4 | `CatalogSearchQuery`: Grenzen, `activeFilterCount`; URL-Parameter lesen/schreiben (Rundreise, ungültige Werte ignoriert); Controller: Filteränderung startet ab Versatz 0 | Filterbereich: jede Filterart setzt den Wert; Zurücksetzen; Bottom Sheet (kompakt) und Seitenleiste (≥ 1024 dp); Begriffe nach Sprache sortiert und benannt; Fehler beim Laden der Begriffe | Web: Filter Genre + Spielerzahl mit Suchbegriff | je Filter allein; Kombination aller Filter (UND); zwei Genres → nur Spiele mit beiden (Spec-F1); fehlende Angaben werden ausgeschlossen, Dauer mit nur `max_minutes` bzw. nur `min_minutes` zählt (Spec-F2); unbekannte Genre-ID → 0 Treffer; Duplikate im Array zählen einfach | B1, B2, F1, F2, U1, T1, T2 |
|
||||
| Krit. 5 | Controller: `loadMore` hängt an, verwirft doppelte IDs, nicht doppelt gleichzeitig, kein Nachladen über Versatz 10 000; Fehler beim Nachladen behält Treffer; `retry` | Listenfuß: Ladeanzeige, „Weitere laden“, Fehler + „Erneut versuchen“; automatisches Nachladen beim Scrollen; Fokus auf die erste neue Kachel | Web: ≥ 31 Fixture-Treffer, Scrollen lädt die zweite Seite; „Weitere laden“ per Tastatur | Seiten ohne Überschneidung, Vereinigung = Gesamtmenge, Reihenfolge stabil über Aufrufe; `total_count` gleich in allen Zeilen und Seiten | B2, F2, T1, T2 |
|
||||
| Krit. 6 | – | Leerzustand mit Suchbegriff: Hinweis, Text mit Suchbegriff, „Filter zurücksetzen“ nur mit Filtern, Melden-Einstieg nur bei gesetztem Callback | Web: Suche ohne Treffer zeigt Hinweis | – | F3, U1, T2 |
|
||||
| Krit. 7 | – | – | Projekt `perf` gegen Staging (7.3); lokal: Datenbank-Budget mit synthetischen Daten | – (Messung, nicht pgTAP) | B5, T3, D1, C1 |
|
||||
| Krit. 7 | – | – | Projekt `perf` (7.3) nach Spec-F4; Datenbank-Budget lokal als `anon` mit synthetischen Daten | – (Messung, nicht pgTAP) | B5, T3, D1, C1 |
|
||||
| Krit. 8 | Controller: ohne Suchbegriff mit Filtern; Startzustand ohne beides | Leerzustand nur mit Filtern | Web: nur Filter, kein Suchbegriff → passende Spiele | ohne Suchbegriff: Filter wirken, Sortierung nach Titel, `match_rank` `null`; ohne beides: alle sichtbaren Katalogspiele (Fixtures vollständig enthalten) | B2, F2, T1, T2 |
|
||||
| KAT-05 Krit. 4 | – | – | – | ausgeblendete und zusammengeführte Fixture-Spiele erscheinen nie, auch nicht über alternative Titel oder Verlag | B2 |
|
||||
| KAT-05 Krit. 10 | – | Kachel zeigt `GameCover`-Platzhalter, aus dem Semantics-Baum ausgeblendet | – | `image_path` maskiert bei Quelle ohne Erlaubnis (über die View) | B2, T2 |
|
||||
| Sicherheit Krit. 2 | – | – | Web: Suche ohne Anmeldung | `anon` und `authenticated` dürfen `search_catalog_games` ausführen, `public`/`service_role` nicht (`has_function_privilege`); `search_normalize` für keine API-Rolle ausführbar; eigene und fremde private Spiele erscheinen nie (auch mit gleichem Titel); keine Schreibrechte auf `game_alt_titles` | B2 |
|
||||
| Eingabegrenzen | `CatalogSearchQuery` lässt nur zulässige Werte zu | Suchfeld begrenzt auf 100 Zeichen | – | `search_query_too_long`, `search_filter_invalid` (je Parameter, Array > 20), `search_paging_invalid` (`limit` 0/51, `offset` −1/10 001) | B2, T1 |
|
||||
| KAT-05 Krit. 10 | – | Kachel zeigt `GameCover`-Platzhalter (`imageUrl` `null`), aus dem Semantics-Baum ausgeblendet | – | `image_path` maskiert bei Quelle ohne Erlaubnis (über die View) | B2, T2 |
|
||||
| Sicherheit Krit. 2 | – | – | Web: Suche ohne Anmeldung | `anon` und `authenticated` dürfen `search_catalog_games` ausführen, `public`/`service_role` nicht (`has_function_privilege`); **`search_normalize` ist für `anon`, `authenticated` und `service_role` ausführbar**; **`authenticated` legt ein privates Spiel an und ändert den Titel, `title_search` ist danach gefüllt bzw. aktualisiert (kein 42501)**; **Suche als `anon` (`set local role anon`) läuft ohne Rechtefehler**; eigene und fremde private Spiele erscheinen nie (auch mit gleichem Titel); keine Schreibrechte auf `game_alt_titles` | B2 |
|
||||
| Eingabegrenzen | `CatalogSearchQuery` lässt nur zulässige Werte zu | Suchfeld begrenzt auf 100 Zeichen | – | `search_query_too_long` (101 Zeichen, 11 Wörter), `search_filter_invalid` (je Parameter, Array > 20), `search_paging_invalid` (`limit` 0/51, `offset` −1/10 001) | B2, T1 |
|
||||
| Zustände Krit. 1–3 | Fehlerabbildung: Netzwerkfehler, Zeitlimit, P0001, sonstige; Suchbegriff/Filter bleiben nach Fehler | alle Zustände aus 5.6; „Erneut versuchen“ | Web: `context.setOffline(true)` → Offline-Hinweis, danach online → „Erneut versuchen“ liefert Treffer | – | F1, F2, T1, T2 |
|
||||
| QUE-04 | Logger erhält bei Fehlern keinen Suchbegriff | – | – | – | F1, T1 |
|
||||
| QUE-04 | Logger erhält bei Fehlern keinen Suchbegriff | – | – | – (B5 prüft Protokoll-Einstellungen der Datenbank) | F1, T1, B5 |
|
||||
| QUE-05 Krit. 2–4 | `CatalogTerm.localizedName` (Rückfall Englisch) | Screen in de und en; Trefferzahl Plural | – | – | F1, T2 |
|
||||
| QUE-06 Krit. 1–4 | – | a11y-Guidelines (`labeledTapTargetGuideline`, `androidTapTargetGuideline`, `iOSTapTargetGuideline`, `textContrastGuideline`); `textScaler` 2.0 ohne Überlauf (kompakt und groß); Ansage der Trefferzahl | Web: Bedienung nur per Tastatur | – | U1, T2, T4 |
|
||||
| QUE-06 Krit. 1–4 | – | a11y-Guidelines (`labeledTapTargetGuideline`, `androidTapTargetGuideline`, `iOSTapTargetGuideline`, `textContrastGuideline`); `textScaler` 2.0 ohne Überlauf (kompakt und groß); Ansage der Trefferzahl nur nach abgeschlossener Suche, entprellt | Web: Bedienung nur per Tastatur | – | U1, T2, T4 |
|
||||
| Plattformen Krit. 2 | – | Layout ≥ 1024 dp mit Seitenleiste | – | – | U1, T2 |
|
||||
|
||||
### 7.2 E2E (Projekt `web`, lokal und CI)
|
||||
|
||||
- Neue Datei `e2e/tests/web/kat01-suche.spec.ts`. Testdaten legt ein Helfer über `e2e/support/db.ts` (nur lokal, bestehende Host-Prüfung) als `postgres` an: 40 Katalogspiele mit dem Kunstwort im Titel, zwei Verlagen, alternativen Titeln, Genres und Mechaniken aus dem Vokabular; Aufräumen nach dem Lauf über die festen IDs. Tests laufen ohne Anmeldung.
|
||||
- **Parallelität:** Die Konfiguration läuft mit `fullyParallel`. Die Suite nutzt gemeinsame Fixtures; sie läuft deshalb mit `test.describe.configure({ mode: 'serial' })` oder legt die Fixtures in einem eigenen Setup-Projekt einmal an (Wahl durch test-engineer). Andere Suites bleiben parallel.
|
||||
- Fälle: Krit. 1, 2, 3, 4, 5, 6, 8, Offline (siehe Matrix), Tastaturbedienung.
|
||||
- `supabase/catalog/testdata` reicht nicht (3 Spiele); der echte Startbestand ist für E2E nicht verlässlich (Inhalt wächst).
|
||||
|
||||
### 7.3 Messung Krit. 7 (Projekt `perf`)
|
||||
|
||||
- Eigenes Playwright-Projekt `perf` in `e2e/playwright.config.ts`, nur registriert, wenn `PERF_WEB_URL` gesetzt ist (Muster `throttle`/`spike`). **Nur lesend**, kein Zugriff auf `db.ts`, keine Anmeldung, keine Schlüssel außer dem öffentlichen Anon-Key im Web-Build.
|
||||
- **Konfiguration:** Der `webServer` in `e2e/playwright.config.ts` und die Standardwerte in `e2e/support/auth-config.ts` werden bedingt, sodass ein Lauf nur mit `perf` keinen lokalen Web-Server startet und keine lokalen Auth-Konstanten voraussetzt. `e2e-run.sh` listet weiterhin nur `api`, `web`, `throttle`; `perf` läuft ausschließlich per `workflow_dispatch` bzw. manuell.
|
||||
- Drosselung über CDP `Network.emulateNetworkConditions`: `latency` 100 ms, Download und Upload je 10 Mbit/s (1 250 000 Byte/s).
|
||||
- Ablauf: Web-App von Staging öffnen (Startbestand eingespielt), Semantics einschalten; aus einer festen Liste von mindestens 100 Suchen (vollständige Titel, Teile, alternative Titel, Verlage, Wortkombinationen, nur Filter) je Suche das Feld leeren, den Begriff tippen, **Zeit ab dem letzten Tastendruck bis zur Sichtbarkeit der ersten Treffer-Kachel** (bzw. des Leerzustands bei absichtlich leeren Suchen) messen. Die erste Suche nach dem Laden der App zählt mit (Verbindungsaufbau).
|
||||
- Ablauf: Web-App öffnen, Semantics einschalten; aus einer festen Liste von mindestens 100 Suchen (vollständige Titel, Teile, alternative Titel, Verlage, Wortkombinationen, nur Filter) je Suche das Feld leeren, den Begriff tippen, **Zeit ab dem letzten Tastendruck bis zur Sichtbarkeit der ersten Treffer-Kachel** (bzw. des Leerzustands bei absichtlich leeren Suchen) messen. Die erste Suche nach dem Laden der App zählt mit (Verbindungsaufbau).
|
||||
- **Umgebung (Spec-F4):**
|
||||
- **Vorläufig (solange es kein Staging gibt):** lokaler bzw. CI-Stack (`supabase start`, Web-Build) mit Startbestand und zusätzlich synthetischem Volumen in der Größenordnung von B5 (Ladeskript nur lokal), Drosselung wie oben. Ergebnis im PR 3c.
|
||||
- **Maßgeblich:** dieselbe Messung gegen Staging, sobald es existiert (D1). Sie wird als Freigabebedingung in die Release-Checkliste `docs/release-checkliste.md` (PR #54) aufgenommen.
|
||||
- Ergebnis: p50, p95, Maximum als Testausgabe und Anhang; **bestanden, wenn p95 ≤ 2000 ms**.
|
||||
- Mobile Apps: Stichprobe von 20 Suchen auf einem Android-Gerät oder Emulator mit 4G-Profil (Emulator-Netzwerkprofil bzw. Network Link Conditioner auf iOS), manuell durch Chris oder test-engineer, Ergebnis im PR 3c. Die Web-Messung gilt als maßgeblicher Nachweis; Abweichung im PR begründet.
|
||||
- Ausführung manuell (`workflow_dispatch`) bzw. lokal gegen Staging, nicht bei jedem PR.
|
||||
|
||||
**Nicht automatisiert testbar, manuelle Prüfung:** Krit. 7 auf Mobilgeräten (Stichprobe), Screenreader-Durchgang (TalkBack, VoiceOver, NVDA/VoiceOver im Web), inhaltliche Plausibilität der Relevanz mit dem echten Startbestand (Chris).
|
||||
|
||||
|
|
@ -441,46 +480,48 @@ Werkzeuge: pgTAP (`supabase test db`), Deno-Tests (Startbestand-Werkzeug), `flut
|
|||
|
||||
| # | Agent | Aufgabe | abhängig von |
|
||||
|---|---|---|---|
|
||||
| B1 | backend-dev | Migration `<ts>_catalog_search.sql` nach 3.2: Erweiterungen, `search_normalize`, generierte Spalten + Trigramm-Indizes, `game_alt_titles` (Checks, Unique, Trigger `set_updated_at` und `game_links_mark_manual('alt_titles')`, RLS, Grants), `manual_fields`-Check mit vollständiger Liste + `alt_titles`, RPC `search_catalog_games` nach 4.2 (Prüfungen, Treffer-Regel, Relevanz, Filter, Sortierung, Rechte: `revoke` von allen, dann `grant execute` an `anon`, `authenticated`) | – |
|
||||
| B2 | backend-dev | pgTAP `09_catalog_search.test.sql` nach 7.1 (inkl. Idempotenz der alternativen Titel über die `.inc`-Fixtures); `00_schema.test.sql` Tabellenzahl; Fremdschlüssel-Wächter in `07_catalog_merge.test.sql` um `game_alt_titles.game_id` („bewusst ausgelassen“) ergänzen (3.3); `06` Z. 532–535 nur anpassen, falls B3 Testspiel A ändern muss | B1, B3 |
|
||||
| B3 | backend-dev | Startbestand-Werkzeug nach 4.3: `alt_titles` parsen, prüfen, SQL erzeugen; Deno-Tests (neu und geänderter Test Z. 253); Testdaten-CSV: alternative Titel (de und en) an **Testspiel B**, nicht an Testspiel A (§ 7, Auswirkung auf `06`); alle erzeugten Dateien neu erzeugen | B1 |
|
||||
| B4 | backend-dev | Doku nach 3.4: `docs/datenmodell-r1.md`, `supabase/README.md`, `docs/katalog-pflege.md` (Abschnitt „Alternative Titel pflegen“, SQL-Bausteine wie in pgTAP ausgeführt) | B1, B3 |
|
||||
| B5 | backend-dev | Annahmen prüfen und messen: Erweiterungen lokal verfügbar (und mit devops auf Staging, D1); `statement_timeout` von `anon`/`authenticated`; Skript `supabase/tests/perf/catalog_search_perf.sql` mit synthetischen Daten (nur lokal, in einer zurückgerollten Transaktion), `explain (analyze, buffers)` und p95 im PR dokumentieren | B1 |
|
||||
| B1 | backend-dev | Migration `<ts>_catalog_search.sql` nach 3.2: Erweiterungen, `search_normalize` (Schritte und Rechte nach 3.2 Nr. 2, `execute` für `anon`, `authenticated`, `service_role`), generierte Spalten + Trigramm-Indizes, `game_alt_titles` (Checks, Unique, Trigger `set_updated_at` und `game_links_mark_manual('alt_titles')`, RLS, Grants), `manual_fields`-Check mit vollständiger Liste + `alt_titles`, RPC `search_catalog_games` nach 4.2 (Prüfungen, Treffer-Regel, Relevanz, Filter, Sortierung, Kommentar zum Partial-Index, Rechte: `revoke` von allen, dann `grant execute` an `anon`, `authenticated`) | 2b gemergt (3.3) |
|
||||
| B2 | backend-dev | pgTAP `09_catalog_search.test.sql` nach 7.1 (inkl. Rechte von `search_normalize`, privates Spiel als `authenticated`, Suche als `anon`, Idempotenz der alternativen Titel über die `.inc`-Fixtures); `00_schema.test.sql` Tabellenzahl; Fremdschlüssel-Wächter in `07_catalog_merge.test.sql` um `game_alt_titles.game_id` („bewusst ausgelassen“) ergänzen (3.3); `06` Z. 532–535 nur anpassen, falls B3 Testspiel A ändern muss | B1, B3 |
|
||||
| B3 | backend-dev | Startbestand-Werkzeug nach 4.3: `alt_titles` parsen, prüfen (inkl. `;` verboten), SQL erzeugen; Deno-Tests (neu und geänderter Test Z. 253); Testdaten-CSV: alternative Titel (de und en) an **Testspiel B**, nicht an Testspiel A; alle erzeugten Dateien neu erzeugen | B1 |
|
||||
| B4 | backend-dev | Doku nach 3.4: `docs/datenmodell-r1.md`, `supabase/README.md`, `docs/katalog-pflege.md` (Abschnitte „Alternative Titel pflegen“, „Nach einem Image-Upgrade: Normalisierung prüfen“ mit der Prüfabfrage aus R6, Schritt im Abschnitt Zusammenführen aus 9.5; SQL-Bausteine wie in pgTAP ausgeführt) | B1, B3 |
|
||||
| B5 | backend-dev | Annahmen prüfen und messen: Erweiterungen lokal verfügbar (Staging, sobald vorhanden, mit devops, D1); Rechte der API-Rollen auf `extensions.unaccent`/`similarity`; `statement_timeout` von `anon`/`authenticated`; Protokoll-Einstellungen (`log_min_duration_statement`, `log_statement`, `log_parameter_max_length`, `log_parameter_max_length_on_error`) – Suchbegriffe dürfen nicht in Datenbank-Logs landen, sonst Einstellung bzw. Abweichung dokumentieren; Skript `supabase/tests/perf/catalog_search_perf.sql` mit synthetischen Daten (nur lokal, zurückgerollte Transaktion), Messung **als `anon` mit RLS**, `explain (analyze, buffers)` und p95 im PR dokumentieren; bei Verfehlen die Stufen aus 4.2 Leistung | B1 |
|
||||
| T1a | test-engineer | Review der pgTAP- und Deno-Abdeckung gegen 7.1, Lücken ergänzen | B2, B3 |
|
||||
|
||||
### 8.2 PR 3b – App (R1.1), Branch `feature/3b-katalog-suche-app`, `Refs #3`
|
||||
|
||||
| # | Agent | Aufgabe | abhängig von |
|
||||
|---|---|---|---|
|
||||
| U1 | ui-designer | Entwurf Suche (kompakt, mittel, groß), Treffer-Kachel, Filterbereich (Sheet und Seitenleiste), alle Zustände; gemeinsame Komponenten `AppStateMessage` und Listenfuß in `core/widgets/`; ARB-Texte de/en; Eintrag in `docs/design-system.md` | – (Vertrag § 4.2 und 5.6) |
|
||||
| F1 | flutter-dev | Domäne und Daten nach 5.1–5.3: Modelle ohne `toJson`, `CatalogRepository`, `SupabaseCatalogRepository` (Parameterbau, Zeitlimit, Fehlerabbildung, Logging ohne Suchbegriff), DTOs, Provider | – (gegen den Vertrag § 4.2; Integration gegen echte RPC nach 3a) |
|
||||
| F2 | flutter-dev | `CatalogSearchController` und `CatalogSearchState` nach 5.4 (Entprellung, letzte Anfrage zählt, Nachladen, Wiederholen) | F1 |
|
||||
| F3 | flutter-dev | Route `/catalog` mit Query-Parametern, URL-Abgleich (5.5, mit Rückfall), `CatalogSearchScreen`/`CatalogSearchView` nach 5.6 mit den Komponenten aus U1, Einstieg auf der Startseite, `onTap = null`, Melden-Callback `null` | F2, U1 |
|
||||
| U1 | ui-designer | Entwurf Suche (kompakt, mittel, groß), Treffer-Kachel, Filterbereich (Sheet und Seitenleiste), alle Zustände; gemeinsame Komponenten `AppStateMessage` und Listenfuß in `core/widgets/`; Ansage- und Fokusverhalten (5.6); ARB-Texte de/en; Eintrag in `docs/design-system.md` | – (Vertrag § 4.2 und 5.6) |
|
||||
| F1 | flutter-dev | Domäne und Daten nach 5.1–5.3: Modelle ohne `toJson`, DTOs nur `json_serializable`, `CatalogRepository`, `SupabaseCatalogRepository` (Parameterbau, Zeitlimit, Fehlerabbildung, Logging ohne Suchbegriff), Provider | – (gegen den Vertrag § 4.2; Integration gegen echte RPC nach 3a) |
|
||||
| F2 | flutter-dev | `CatalogSearchController` und `CatalogSearchState` nach 5.4 (Entprellung, letzte Anfrage zählt, Nachladen bis Versatz 10 000, Wiederholen) | F1 |
|
||||
| F3 | flutter-dev | Route `/catalog` mit Query-Parametern, URL-Abgleich (5.5, `Router.neglect` + `SystemNavigator.routeInformationUpdated(replace: true)`, mit Rückfall), `CatalogSearchScreen`/`CatalogSearchView` nach 5.6 mit den Komponenten aus U1, Einstieg auf der Startseite, `onTap = null`, `imageUrl = null`, Melden-Callback `null` | F2, U1 |
|
||||
| T1 | test-engineer | Unit-Tests (Repository mit gemocktem Client, DTO, Fehlerabbildung, Query, URL-Rundreise, Controller mit `fakeAsync`) | F1, F2 |
|
||||
| T2 | test-engineer | Widget-Tests nach 7.1 (Zustände, Filter in beiden Layouts, Listenfuß, a11y-Guidelines, `textScaler` 2.0, de/en) | F3 |
|
||||
| T2 | test-engineer | Widget-Tests nach 7.1 (Zustände, Filter in beiden Layouts, Listenfuß inkl. Fokus, Ansage, a11y-Guidelines, `textScaler` 2.0, de/en) | F3 |
|
||||
|
||||
### 8.3 PR 3c – E2E und Messung (R1.1), Branch `feature/3c-katalog-suche-e2e`, `Closes #3` (siehe Spec-F4)
|
||||
### 8.3 PR 3c – E2E und Messung (R1.1), Branch `feature/3c-katalog-suche-e2e`, `Closes #3` nur bei Spec-F4 A, sonst `Refs #3`
|
||||
|
||||
| # | Agent | Aufgabe | abhängig von |
|
||||
|---|---|---|---|
|
||||
| T4 | test-engineer | Web-E2E `kat01-suche.spec.ts` nach 7.2 inkl. Fixture-Helfer und Aufräumen | 3a, 3b gemergt |
|
||||
| T3 | test-engineer | Projekt `perf` nach 7.3 (Drosselung, Suchliste, Auswertung p50/p95) | 3b gemergt |
|
||||
| D1 | devops | Staging bereitstellen bzw. aktualisieren: Migrationen, Web-Build, Erweiterungen prüfen (B5); CI-Job für das E2E-Projekt `web` um die neue Datei ergänzen (falls nicht automatisch); `workflow_dispatch`-Job für `perf` mit `PERF_WEB_URL` | 3a gemergt |
|
||||
| C1 | Chris | Startbestand auf Staging einspielen (Spec #2 Runbook), Messung auslösen bzw. abnehmen; mobile Stichprobe; Relevanz mit echten Daten plausibilisieren | D1, T3 |
|
||||
| T4 | test-engineer | Web-E2E `kat01-suche.spec.ts` nach 7.2 inkl. Fixture-Helfer, Aufräumen und serieller Ausführung bzw. Setup-Projekt | 3a, 3b gemergt |
|
||||
| T3 | test-engineer | Projekt `perf` nach 7.3 (bedingter `webServer`/`auth-config.ts`, Drosselung, Suchliste, Ladeskript für synthetisches Volumen lokal, Auswertung p50/p95); vorläufige Messung gegen den lokalen bzw. CI-Stack | 3b gemergt |
|
||||
| D1 | devops | CI-Job für das E2E-Projekt `web` um die neue Datei ergänzen (falls nicht automatisch); `workflow_dispatch`-Job für `perf` mit `PERF_WEB_URL`; Eintrag „Krit. 7 gegen Staging messen“ in `docs/release-checkliste.md` (PR #54); Entscheidung zur Mengenbegrenzung der RPC je IP (4.2 Sicherheit) im Hosting-Runbook vor dem Go-live; sobald Staging existiert: Migrationen, Web-Build, Erweiterungen prüfen (B5) | 3a gemergt |
|
||||
| C1 | Chris | Vorläufige Messung abnehmen (Spec-F4 A); später Startbestand auf Staging einspielen (Spec #2 Runbook) und Staging-Messung vor dem Release abnehmen; mobile Stichprobe; Relevanz mit echten Daten plausibilisieren | T3 (Staging: D1) |
|
||||
|
||||
### 8.4 Reihenfolge und Parallelität
|
||||
|
||||
```
|
||||
#2 2b gemergt → 2c gemergt (Voraussetzung für den Merge von 3a, 3.3)
|
||||
#2 2b gemergt (Voraussetzung für den Merge von 3a; 2c davor oder danach, 3.3)
|
||||
3a B1 → B3, B5 (parallel) → B2 → B4 → T1a → Merge (Chris)
|
||||
3b U1 ‖ F1 → F2 → F3 → T1, T2 (startet parallel zu 3a, gegen den Vertrag § 4.2)
|
||||
└─ Integration gegen die echte RPC, sobald 3a gemergt ist → Merge (Chris)
|
||||
3c D1 (nach 3a) → T4, T3 (nach 3b) → C1 → Merge (Chris), Issue #3 geschlossen
|
||||
3c D1 (nach 3a) → T4, T3 (nach 3b) → C1 (vorläufige Messung) → Merge (Chris), #3 geschlossen bei Spec-F4 A
|
||||
später: Staging (D1) → Messung als Release-Freigabebedingung (C1)
|
||||
```
|
||||
|
||||
- Der Vertrag in § 4.2 ist verbindlich für 3a und 3b; Änderungen daran nur über diese Spec.
|
||||
- 3b wird erst nach 3a gemergt (sonst ruft die App eine fehlende RPC auf).
|
||||
- Merge-Reihenfolge im Backend: **2b → 2c → 3a** (3.3). Arbeiten an 3a und 3b dürfen vorher beginnen; vor dem PR von 3a wird auf `main` aktualisiert.
|
||||
- Backend: **3a nach 2b**; wer von 2c und 3a später merged, gleicht Fremdschlüssel-Wächter, Tabellenzahl und `manual_fields`-Check ab (3.3). Arbeiten an 3a und 3b dürfen vorher beginnen; vor dem PR von 3a wird auf `main` aktualisiert.
|
||||
- #13 (Spec auf `docs/13-kat04-spec`) setzt `search_normalize` aus 3a voraus; 13a wird nach 3a gemergt.
|
||||
- Merge nur durch Chris. Kein ADR (9.4).
|
||||
|
||||
## 9. Risiken & offene Fragen
|
||||
|
|
@ -489,27 +530,28 @@ Werkzeuge: pgTAP (`supabase test db`), Deno-Tests (Startbestand-Werkzeug), `flut
|
|||
|
||||
| # | Risiko | Auswirkung | Gegenmaßnahme |
|
||||
|---|---|---|---|
|
||||
| R1 | `pg_trgm` oder `unaccent` sind im self-hosted Image nicht verfügbar (unverifiziert) | Migration schlägt auf Staging/Produktion fehl | B5 prüft vor dem Merge von 3a. Rückfall ohne `unaccent`: `search_normalize` mit `translate()` und `replace()` für die gängigen Zeichen (äöüàáâéèêíóúñç…, ß → ss), ebenfalls `immutable`. Rückfall ohne `pg_trgm`: keine Trigramm-Indizes, Sortierung ohne Ähnlichkeit (Titel, ID); bei R1-Größe ausreichend schnell. |
|
||||
| R2 | Abfrage mit mehreren `exists`/`or` über drei Tabellen wird bei wachsendem Katalog langsam | Krit. 7 verfehlt | Budget 150 ms bei 10 000 Spielen (B5); Rückfall: zusammengefasster Suchtext je Spiel in einer eigenen Spalte, gepflegt per Trigger an `games`, `game_alt_titles`, `game_publishers`, `publishers` |
|
||||
| R3 | Staging existiert noch nicht oder hat keinen Startbestand (ADR-0002: ein Server für Staging/Produktion) | Krit. 7 nicht abnehmbar, #3 bleibt offen | D1 früh einplanen; Startbestand-Fixtures für die Messung reichen nicht, es braucht die echte Größenordnung |
|
||||
| R4 | `search_catalog_games` ist ohne Anmeldung aufrufbar | Last durch automatisierte Aufrufe | Eingabegrenzen, `statement_timeout` (B5), nur lesend; Mengenbegrenzung am Gateway erst bei Bedarf (nicht in R1) |
|
||||
| R1 | `pg_trgm` oder `unaccent` sind im self-hosted Image nicht verfügbar (unverifiziert) | Migration schlägt auf Staging/Produktion fehl | B5 prüft vor dem Merge von 3a. Rückfall ohne `unaccent`: `search_normalize` mit `translate()` und `replace()` für die gängigen Zeichen (äöüàáâéèêíóúñç…, ß → ss, æ → ae), ebenfalls `immutable`. Rückfall ohne `pg_trgm`: keine Trigramm-Indizes, Sortierung ohne Ähnlichkeit (Titel, ID); bei R1-Größe ausreichend schnell. |
|
||||
| R2 | Die Abfrage (2-Zeichen-`like`, Regel ohne Leerzeichen, `or` über `exists`, RLS für `anon`) nutzt die Trigramm-Indizes kaum und wird bei wachsendem Katalog langsam | Krit. 7 verfehlt | Messung als `anon` (B5); Stufen aus 4.2 Leistung (Kandidaten-CTE, `title_compact`); letzter Rückfall: zusammengefasster Suchtext je Spiel in einer eigenen Spalte, gepflegt per Trigger an `games`, `game_alt_titles`, `game_publishers`, `publishers` |
|
||||
| R3 | **Staging existiert noch nicht** (ADR-0002: ein Hetzner-Server für Staging/Produktion) | Krit. 7 lässt sich nicht wie formuliert („gegen Staging“) abnehmen | Spec-F4: vorläufige Messung lokal/CI, Staging-Messung als Freigabebedingung in der Release-Checkliste; D1 früh einplanen |
|
||||
| R4 | `search_catalog_games` ist ohne Anmeldung aufrufbar; viele parallele Aufrufe | Last auf der Datenbank | Eingabegrenzen, `statement_timeout` der Rollen (B5; `set statement_timeout` an der Funktion wirkt nicht), nur lesend; Mengenbegrenzung je IP (Kong `rate-limiting` oder `limit_req` im Proxy) als Entscheidung vor dem Go-live (D1), kein Muss für R1.1. Das vollständige Durchblättern des öffentlichen Katalogs ist gewollt. |
|
||||
| R5 | Nachladen per `offset` bei gleichzeitiger Katalogänderung | Ein Spiel doppelt oder übersprungen | Doppelte IDs werden verworfen; Katalog ändert sich selten; akzeptiert |
|
||||
| R6 | Änderung von `search_normalize` später | Generierte Spalten veralten nicht automatisch | Kommentar an der Funktion; Neuanlage der Spalten per Migration |
|
||||
| R7 | Parallelarbeit mit 2b/2c und #12 | Konflikte bei `manual_fields`-Check, Tabellenzahl in `00_schema`, Fremdschlüssel-Wächter | Merge-Reihenfolge 2b → 2c → 3a (3.3); Regel „vollständige Liste“ (3.2 Nr. 5); Tabellenzahl beim Merge abgleichen; Wächter-Einordnung von `game_alt_titles` durch 3a (3.3) |
|
||||
| R6 | `search_normalize` ändert sich, oder ein Image-Upgrade ändert `unaccent.rules` | Generierte Spalten weichen von der Funktion ab, Suchen treffen nicht mehr | Kommentar an der Funktion; **Runbook-Schritt nach jedem Image-Upgrade:** `select count(*) from public.games where title_search is distinct from public.search_normalize(title)` (ebenso für `game_alt_titles.title_search` und `publishers.name_search`); bei Abweichung die Spalten per Migration neu anlegen (B4) |
|
||||
| R7 | Parallelarbeit mit 2b, 2c, #12 und #13 | Konflikte bei `manual_fields`-Check, Tabellenzahl in `00_schema`, Fremdschlüssel-Wächter, Normalisierung | 3a nach 2b; wer von 2c und 3a später merged, gleicht ab (3.3); Regel „vollständige Liste“ (3.2 Nr. 5); eine Basisfunktion `search_normalize` mit gleichlautendem Satz in #3 und #13 (3.2 Nr. 2) |
|
||||
| R8 | Relevanz wirkt mit echten Daten unpassend | Nutzer finden das erwartete Spiel weiter unten | Stufen sind einfach anpassbar (nur RPC); Plausibilisierung durch Chris (C1) |
|
||||
| R9 | Schreibweise „ae“/„oe“/„ue“ findet Umlaut-Titel nicht | „Kaese“ findet „Käse“ nicht | Spec-F5; spätere Erweiterung ohne Datenmigration der Clients möglich (nur Funktion + Spalten) |
|
||||
| R10 | Krit. 7 wird im Web gemessen, gilt aber für mobile Apps | Abweichung der Messbedingung | Web als maßgeblicher Nachweis, mobile Stichprobe (7.3), Begründung im PR |
|
||||
| R11 | Suchbegriffe landen in Datenbank-Logs (langsame Anweisungen, Parameter bei Fehlern) | Verstoß gegen „nicht protokolliert“ (QUE-04) | B5 prüft `log_min_duration_statement` und `log_parameter_max_length*`; Ergebnis bzw. nötige Einstellung im PR und im Hosting-Runbook |
|
||||
|
||||
### 9.2 Offene Fragen an Chris (keine blockiert die Umsetzung)
|
||||
|
||||
| # | Frage | Optionen | Empfehlung | Wirkung |
|
||||
|---|---|---|---|---|
|
||||
| Spec-F1 | Mehrere Genres (bzw. Mechaniken) gewählt: muss ein Spiel **alle** haben oder **eines davon**? | A: alle (UND); B: eines davon (ODER innerhalb einer Filterart, UND zwischen Filterarten) | **A**, wörtliche Lesart von Krit. 4/8 („zu allen gewählten Filtern“, Klärung 13). B liefert beim Entdecken mehr Treffer; Umstellung später nur in der RPC. | E7, 4.2, 7.1 Krit. 4 |
|
||||
| Spec-F2 | Bedeutung von Spielerzahl, Dauer und Alter als Filter; Umgang mit fehlenden Angaben | Spielerzahl: Zahl n, passt wenn n im Bereich min–max. Dauer: „bis X Minuten“ (30/45/60/90/120/180), passt wenn die längste Angabe ≤ X; alternativ Bereiche („kurz/mittel/lang“). Alter: „ab X Jahren geeignet“, passt wenn Mindestalter ≤ X. Fehlende Angabe: ausschließen oder einschließen. | **Wie links beschrieben** (n im Bereich; „bis X Minuten“; Mindestalter ≤ X) und **fehlende Angaben ausschließen** (ein Spiel ohne Spielerzahl passt nicht nachweislich). Im Startbestand sind alle Angaben Pflicht (Spec #2 SB2), die Regel betrifft nur spätere Einträge. | 4.2, 5.2, 5.6, 7.1 |
|
||||
| Spec-F2 | Bedeutung von Spielerzahl, Dauer und Alter als Filter; Umgang mit fehlenden Angaben | Spielerzahl: Zahl n, passt wenn n im Bereich min–max. Dauer: „bis X Minuten“ (30/45/60/90/120/180), passt wenn die längste bekannte Angabe ≤ X (auch wenn nur `max_minutes` oder nur `min_minutes` gepflegt ist); alternativ Bereiche („kurz/mittel/lang“). Alter: „ab X Jahren geeignet“, passt wenn Mindestalter ≤ X. Fehlende Angabe: ausschließen oder einschließen. | **Wie links beschrieben** (n im Bereich; „bis X Minuten“; Mindestalter ≤ X) und **fehlende Angaben ausschließen** (ein Spiel ohne Spielerzahl passt nicht nachweislich). Im Startbestand sind alle Angaben Pflicht (Spec #2 SB2), die Regel betrifft nur spätere Einträge. | 4.2, 5.2, 5.6, 7.1 |
|
||||
| Spec-F3 | Was zeigt die Suche ohne Suchbegriff und ohne Filter? | A: den ganzen Katalog alphabetisch; B: nur einen Hinweis „Suchbegriff eingeben oder Filter wählen“ | **A** (Entdecken ohne Sonderfall, E8) | E8, 4.2, 5.6 |
|
||||
| Spec-F4 | Krit. 6 („Möglichkeit, das Spiel zu melden“) hängt an KAT-04 (#13). Wann gilt #3 als erledigt? | A: #3 liefert Hinweis + Einstiegspunkt; Krit. 6 wird im PR als „Hinweis erfüllt, Melden mit #13“ abgehakt, #3 wird mit 3c geschlossen, #13 übernimmt die Aktion als eigenes Kriterium; B: #3 bleibt offen, bis #13 fertig ist | **A** (entspricht der Teilabhängigkeit im Issue; bei B hinge der Abschluss von #3, einer R1.1-Story, an #13 aus R1.2) | E11, 5.6, 8.3 |
|
||||
| Spec-F4 | **Wann gilt #3 als erledigt?** Zwei Teilabhängigkeiten: Krit. 6 hängt an #13 (Melden), Krit. 7 verlangt eine Messung „gegen Staging“, **Staging existiert aber noch nicht** (ADR-0002: ein Hetzner-Server). | **A:** PR 3c schließt #3. Krit. 6: Hinweis + Einstiegspunkt in #3, im PR als „Hinweis erfüllt, Melden mit #13“ abgehakt, #13 übernimmt die Aktion. Krit. 7: vorläufige Messung mit dem Projekt `perf` gegen den lokalen bzw. CI-Stack mit Startbestand und synthetischem Volumen, Drosselung wie in 7.3; die Messung gegen Staging wird **Freigabebedingung in der Release-Checkliste** (`docs/release-checkliste.md`, PR #54). **B:** #3 bleibt offen, bis #13 umgesetzt und die Messung gegen Staging erfolgt ist (beide Teile auch einzeln wählbar). | **A** (entspricht der Teilabhängigkeit im Issue; das Release kann ohne Staging-Messung trotzdem nicht freigegeben werden; bei B hinge der Abschluss einer R1.1-Story an #13 aus R1.2 und an der Infrastruktur) | E11, E12, 5.6, 7.3, 8.3 |
|
||||
| Spec-F5 | Soll „ae/oe/ue/ss“ als Schreibweise für „ä/ö/ü/ß“ in R1 treffen (z. B. „Kaese“ → „Käse“)? | A: nein in R1 (nur Akzente/Umlaute entfernen); B: ja (zweite normalisierte Form je Titel) | **A**; Krit. 1 verlangt Groß/Klein und Sonderzeichen. B ist später ohne App-Änderung nachrüstbar (R9). | 2 (Konkretisierung), 3.2 Nr. 2 |
|
||||
| Spec-F6 | Alternative Titel beim Zusammenführen zweier Katalogspiele (2b, `admin_merge_games`) | A: bleiben am ausgeblendeten Quell-Eintrag (wie Genres/Verlage); der Admin ergänzt am Ziel bei Bedarf Titel per SQL (Runbook-Schritt vor dem Zusammenführen); B: werden automatisch auf das Ziel übertragen (ohne Duplikate) | **A** für R1: einfach, kein Eingriff in die freigegebene Spec #2; der Haupttitel der Quelle hat keine Sprache und ließe sich ohnehin nicht automatisch als alternativer Titel übernehmen | 9.5 (2b), Fremdschlüssel-Wächter |
|
||||
| Spec-F6 | Alternative Titel beim Zusammenführen zweier Katalogspiele (2b, `admin_merge_games`) | A: bleiben am ausgeblendeten Quell-Eintrag (wie Genres/Verlage); der Admin ergänzt am Ziel bei Bedarf Titel per SQL (Runbook-Schritt vor dem Zusammenführen); B: werden automatisch auf das Ziel übertragen (ohne Duplikate) | **A** für R1: einfach, kein Eingriff in die freigegebene Spec #2 und in 2b; der Haupttitel der Quelle hat keine Sprache und ließe sich ohnehin nicht automatisch als alternativer Titel übernehmen | 3.3, 9.5, Fremdschlüssel-Wächter |
|
||||
| Spec-F7 | Sprachen alternativer Titel | A: nur `de` und `en` (wie `description_locale`); B: beliebige Sprache (ISO 639-1, z. B. Originaltitel auf Französisch) | **A** (Klärung 3 nennt Deutsch und Englisch; erweiterbar per Migration) | 3.2 Nr. 4, 4.3 |
|
||||
|
||||
### 9.3 Designwahlen (zur Bestätigung mit der Freigabe)
|
||||
|
|
@ -517,29 +559,35 @@ Werkzeuge: pgTAP (`supabase test db`), Deno-Tests (Startbestand-Werkzeug), `flut
|
|||
| # | Wahl | Begründung | Verworfene Alternative |
|
||||
|---|---|---|---|
|
||||
| W1 | RPC statt PostgREST-Filter auf `catalog_games` (E1) | Relevanz, UND-Filter über Zuordnungen und Verlagssuche in einer Anfrage | mehrere Anfragen bzw. Filter im Client |
|
||||
| W2 | Generierte Spalten statt Ausdrucksindex (E3) | einfache Abfragen, Normalisierung einmal beim Schreiben | Ausdrucksindex `search_normalize(title)`; Suchtext-Spalte per Trigger (Rückfall R2) |
|
||||
| W2 | Generierte Spalten statt Ausdrucksindex (E3) | einfache Abfragen, Normalisierung einmal beim Schreiben, gemeinsame Grundlage für #3 und #13 | Ausdrucksindex `search_normalize(title)`; Suchtext-Spalte per Trigger (Rückfall R2) |
|
||||
| W3 | `limit`/`offset` mit `total_count` (E6) | einfach, Trefferzahl für Anzeige und Ansage | Keyset-Paging |
|
||||
| W4 | Suche ab 2 Zeichen | ein Zeichen trifft fast alles und belastet die Datenbank ohne Nutzen | ab 1 Zeichen; ab 3 Zeichen (Trigramm-optimal, aber „Go“ wäre nicht suchbar) |
|
||||
| W4 | Suche ab 2 Zeichen, Wörter ab 2 Zeichen | ein Zeichen trifft fast alles und belastet die Datenbank ohne Nutzen | ab 1 Zeichen; ab 3 Zeichen (Trigramm-optimal, aber „Go“ wäre nicht suchbar) |
|
||||
| W5 | Alternative Titel ohne Schreibrecht für Nutzer (E2) | kein Anwendungsfall in R1, keine Angriffsfläche | Schreiben für Ersteller privater Spiele (Muster `game_genres_write`) |
|
||||
| W6 | Datenklassen ohne `toJson` (E9) | macht den Merkpunkt aus Spec #2 technisch unverletzbar | Dokumentation allein |
|
||||
| W7 | Kachel in #3 nicht antippbar, #4 verdrahtet | kein toter Link auf eine fehlende Detailseite | Platzhalter-Detailseite in #3 |
|
||||
| W8 | Keine Suchbegriffe in Logs, Aufruf per `POST` | Datensparsamkeit (QUE-04) | Suchanalytik |
|
||||
| W9 | `search_normalize` für alle API-Rollen ausführbar | generierte Spalten und `security invoker`-RPCs rechnen mit den Rechten des Aufrufers; die Funktion liest keine Daten | `execute` entzogen (bricht Schreiben privater Spiele und die Suche als `anon`) |
|
||||
|
||||
### 9.4 Ausstehende Prozessschritte
|
||||
|
||||
- Spec-Review durch Chris; Label `status/spec-review` ist gesetzt, die Fragen stehen zusätzlich als Kommentar im Issue #3. `needs-clarification` wird nicht gesetzt, weil keine Frage die Umsetzung blockiert (jede hat eine Empfehlung, die Umstellung betrifft jeweils nur die RPC bzw. einen Text).
|
||||
- Spec-Review durch Chris; Label `status/spec-review` ist gesetzt, die Fragen stehen zusätzlich als Kommentar im Issue #3. `needs-clarification` wird nicht gesetzt, weil keine Frage die Umsetzung blockiert (jede hat eine Empfehlung, die Umstellung betrifft jeweils nur die RPC, einen Text oder den Abschluss des Issues).
|
||||
- **Kein ADR nötig:** `pg_trgm` und `unaccent` sind Standard-Erweiterungen von PostgreSQL-contrib im Supabase-Image, ohne neue Dienste, Pakete oder Lizenzen; die Entscheidung lässt sich per Migration zurücknehmen. Die übrigen Entscheidungen bleiben innerhalb von ADR-0001/0002.
|
||||
- Nach Freigabe: B1 (backend-dev) und parallel U1, F1 (ui-designer, flutter-dev); D1 (devops) früh wegen R3.
|
||||
- Nach Freigabe: B1 (backend-dev, nach Merge von 2b) und parallel U1, F1 (ui-designer, flutter-dev); D1 (devops) früh wegen R3/R4.
|
||||
|
||||
### 9.5 Hinweise für andere Stories, nicht Teil von #3
|
||||
|
||||
- **KAT-05 2b/2c (#2):** Keine Änderung an 2b oder 2c nötig, weil beide vor 3a gemergt werden (3.3). 3a ordnet `game_alt_titles.game_id` im Fremdschlüssel-Wächter von `07_catalog_merge.test.sql` als „bewusst ausgelassen“ ein (bleibt am Quell-Eintrag, passt zu `admin_merge_games`, das die Zuordnungen ebenfalls nicht umhängt; Spec-F6 A). Für das Runbook (Abschnitt Zusammenführen) ergänzt 3a (B4) den Schritt „vor dem Zusammenführen relevante Titel der Quelle als alternative Titel am Ziel anlegen“.
|
||||
- **KAT-02 (#4):** Route `/catalog/games/:id`, `onTap` der Treffer-Kachel verdrahten; Domänenmodell `CatalogGame` (Detail) neu, `CatalogGameSummary` bleibt für Listen. Alternative Titel sind in KAT-02 Krit. 1 nicht gefordert.
|
||||
- **KAT-04 (#13):** `onReportGame`-Callback im Leerzustand setzen (Suchbegriff als vorbelegter Titel, Anmeldung über `from`). „Gleicher normalisierter Titel“ (Klärung 15) auf Basis von `search_normalize` plus Entfernen von Artikeln (eigene Funktion, nicht in `search_normalize`, sonst ändert sich die Suche); Trigramm-Ähnlichkeit über `extensions.similarity` auf `title_search`. Krit. 6 von KAT-01 wird mit #13 vollständig (Spec-F4).
|
||||
- **KAT-05 2b/2c (#2):** Keine Änderung an 2b nötig. 3a ordnet `game_alt_titles.game_id` im Fremdschlüssel-Wächter von `07_catalog_merge.test.sql` als „bewusst ausgelassen“ ein (bleibt am Quell-Eintrag, passt zu `admin_merge_games`, das die Zuordnungen ebenfalls nicht umhängt; Spec-F6 A). Wird 2c nach 3a gemergt, übernimmt 2c diesen Eintrag, die Tabellenzahl und die `manual_fields`-Liste von `main` (3.3). Für das Runbook (Abschnitt Zusammenführen) ergänzt 3a (B4) den Schritt „vor dem Zusammenführen relevante Titel der Quelle als alternative Titel am Ziel anlegen“.
|
||||
- **KAT-02 (#4):** Route `/catalog/games/:id`, `onTap` der Treffer-Kachel verdrahten; Domänenmodell `CatalogGame` (Detail) neu, `CatalogGameSummary` bleibt für Listen; Umwandlung von `image_path` in eine URL über die Dateizugriffsschicht. Alternative Titel sind in KAT-02 Krit. 1 nicht gefordert.
|
||||
- **KAT-04 (#13):**
|
||||
- `onReportGame`-Callback im Leerzustand setzen (Suchbegriff als vorbelegter Titel, Anmeldung über `from`). Krit. 6 von KAT-01 wird mit #13 vollständig (Spec-F4).
|
||||
- **Normalisierung:** `public.normalize_game_title(t) = coalesce(<Artikel entfernen>(search_normalize(t)), search_normalize(t), lower(btrim(t)))`, angelegt von #13 über der Basisfunktion aus 3a; Artikel werden nur dort entfernt (gemeinsamer Satz in 3.2 Nr. 2).
|
||||
- **Alternative Titel in `find_similar_catalog_games` (13a):** Die Ähnlichkeitsprüfung („gleicher normalisierter Titel“ bzw. Trigramm-Ähnlichkeit, Klärung 15) bezieht alternative Titel verbindlich über `game_alt_titles.title_search` ein, Trigramm-Ähnlichkeit über `extensions.similarity`.
|
||||
- pgTAP-Datei `10_…` (09 ist #3); 13a wird nach 3a gemergt.
|
||||
- **QUE-09 (#29):** Startseite ohne Anmeldung bettet `CatalogSearchView` ein.
|
||||
- **KTO-02 (#14), SAM-01, PAR-01 (#15):** Katalogsuche über `CatalogRepository.search` wiederverwenden. Wo vorläufige private Spiele wählbar sein sollen (Klärung 36), ist das eine eigene Abfrage auf eigene private Spiele, nicht Teil von `search_catalog_games` (die RPC liefert bewusst nur den Katalog).
|
||||
- **KAT-03 (#12):** neuer `manual_fields`-Schlüssel mit vollständiger Liste inkl. `alt_titles`.
|
||||
- **Import (SAM-08 #25, Import-Spec):** alternative Titel aus der Quelle schreibt der Import als `catalog_importer` (Ausnahme im Trigger greift bereits).
|
||||
- **Hosting/Release (D1):** Mengenbegrenzung für `search_catalog_games` vor dem Go-live entscheiden; Krit. 7 gegen Staging als Freigabebedingung in `docs/release-checkliste.md`; Prüfabfrage aus R6 nach Image-Upgrades.
|
||||
|
||||
## 10. Änderungsprotokoll
|
||||
|
||||
|
|
@ -551,4 +599,17 @@ Erste Fassung durch architect, wartet auf Freigabe und Antworten zu Spec-F1 bis
|
|||
|
||||
- § 7.1 Krit. 2: Idempotenz der alternativen Titel nach pgTAP verschoben (über die erzeugten `.inc`-Fixtures); Deno prüft nur das Skript.
|
||||
- § 7: Auswirkung auf bestehende Tests: `00_schema` nur Tabellenzahl; `06` Z. 532–535 prüft `manual_fields` von Testspiel A exakt → alternative Titel an Testspiel B (B3), sonst Anpassung in B2.
|
||||
- Neuer Abschnitt 3.3: Merge-Reihenfolge 2b → 2c → 3a; 3a erweitert den Fremdschlüssel-Wächter in `07_catalog_merge.test.sql` um `game_alt_titles.game_id` („bewusst ausgelassen“, Spec-F6 A). Abschnitt „Doku zum Datenmodell“ wird 3.4. B2, B3, § 8.4, R7, 9.5 angepasst.
|
||||
- Neuer Abschnitt 3.3 zur Reihenfolge mit 2b/2c; 3a erweitert den Fremdschlüssel-Wächter in `07_catalog_merge.test.sql` um `game_alt_titles.game_id` („bewusst ausgelassen“, Spec-F6 A). Abschnitt „Doku zum Datenmodell“ wird 3.4.
|
||||
|
||||
### 2026-11-03 – Review (code-reviewer)
|
||||
|
||||
- **B-1:** `search_normalize` ist für `anon`, `authenticated` und `service_role` ausführbar (generierte Spalten und `security invoker`); pgTAP-Fall umgedreht, neue Fälle „privates Spiel als `authenticated`“ und „Suche als `anon`“ (3.2, 4.1, 7.1, W9).
|
||||
- **B-2:** Marken- und Schutzzeichen werden vor `unaccent` entfernt; Brüche und Apostroph festgelegt; Pflichtfälle `™`, `½`, `Æon’s End` (3.2 Nr. 2, 7.1).
|
||||
- **B-3:** Eine Basisfunktion `search_normalize` (von #3), `normalize_game_title` in #13 darüber; gleichlautender Satz in beiden Specs; alternative Titel in `find_similar_catalog_games` (13a) über `game_alt_titles.title_search`; Testnummern 09/10; Begründung zu `immutable` richtiggestellt (3.2 Nr. 2, 9.5).
|
||||
- **W-1:** 3a nach 2b; wer von 2c und 3a später merged, gleicht ab (3.3, R7, 8.4).
|
||||
- **W-2:** Spec-F4 neu: Abschluss von #3 mit vorläufiger Messung, Staging-Messung als Release-Freigabebedingung (E12, 7.3, 8.3, R3).
|
||||
- **W-3:** Indizes nicht mehr als Begründung für Geschwindigkeit; Messung als `anon` mit RLS; Stufen Kandidaten-CTE und `title_compact` (3.2 Nr. 3, 4.2, R2, B5).
|
||||
- **W-4:** Begriff < 2 Zeichen = kein Suchbegriff, Wörter mit 1 Zeichen verworfen (4.2).
|
||||
- **W-5:** `statement_timeout` an der Funktion wirkt nicht; Mengenbegrenzung je IP als Entscheidung vor dem Go-live (4.2, R4, D1).
|
||||
- **W-6:** Prüfabfrage nach Image-Upgrades als Runbook-Schritt (R6, B4).
|
||||
- Hinweise: Versatzgrenze und gewollte Durchblätterbarkeit (E8, 4.2, 5.4); `image_path` nur aus der View, `imageUrl` in #3 immer `null` (4.2, 5.2, 5.6); mehr als 10 Wörter → `search_query_too_long`; Dauer mit nur einer Angabe (Spec-F2); `perf`-Konfiguration (7.3); serielle E2E-Fixtures (7.2); Protokoll-Einstellungen (B5, R11); Ansage und Fokus (5.6); URL-Ersetzung mit `Router.neglect` (5.5); `;` in alternativen Titeln verboten (4.3); DTOs nur `json_serializable` (5.1); Kommentar zum Partial-Index (4.2).
|
||||
|
|
|
|||
Loading…
Reference in a new issue