1304 lines
26 KiB
Markdown
1304 lines
26 KiB
Markdown
# Buying Agent – Source & Discovery Specification v0.3
|
||
|
||
**Status:** Review Candidate – vor Implementierung erneut durch den Coding-Agenten zu prüfen
|
||
**Spezifikationsversion:** 0.3
|
||
**Stand:** 2026-10-04
|
||
|
||
**Ersetzt vollständig:** `buying-agent-source-discovery-spec-v0.3.md`
|
||
|
||
**Gilt zusammen mit:**
|
||
1. `buying-agent-v0.1-spec-v1.1.md`
|
||
2. `buying-agent-architecture-addendum-v1.md`
|
||
3. `buying-agent-frontend-mvp-spec-v0.3.md`
|
||
|
||
Bei Widersprüchen innerhalb des Source-/Discovery-Bereichs hat diese v0.3 Vorrang.
|
||
|
||
---
|
||
|
||
# 1. Ziel
|
||
|
||
Diese Spezifikation definiert den Source-/Discovery-Pfad des **funktionierenden MVP**.
|
||
|
||
Sie ist ausdrücklich **kein Skeleton und kein theoretischer POC**.
|
||
|
||
Der MVP gilt in diesem Bereich erst dann als funktionsfähig, wenn der reale Pfad:
|
||
|
||
```text
|
||
Produktkategorie wählen
|
||
↓
|
||
SourceCategory bestimmen
|
||
↓
|
||
Geizhals-Taxonomie technisch lernen
|
||
↓
|
||
Filter und URL-Konstrukte verifizieren
|
||
↓
|
||
Kriterien über API/UI anbieten
|
||
↓
|
||
Such-URL aus ProjectCriteria erzeugen
|
||
↓
|
||
SearchRun ausführen
|
||
↓
|
||
Ergebnisse und Attribute parsen
|
||
↓
|
||
Produkte identifizieren / historisieren
|
||
↓
|
||
Diffing
|
||
↓
|
||
Kandidaten / Ranking / Shortlist
|
||
↓
|
||
UI
|
||
```
|
||
|
||
end-to-end funktioniert.
|
||
|
||
Initiale Source: **Geizhals**
|
||
Pilotkategorie: **Notebooks**
|
||
|
||
Die Architektur darf nicht notebook-spezifisch sein.
|
||
|
||
---
|
||
|
||
# 2. Grundprinzipien
|
||
|
||
1. Geizhals ist Discovery-Quelle, nicht Decision Engine.
|
||
2. Source-Kriterien bilden für unterstützte Kategorien zunächst den primären Kriterienkatalog.
|
||
3. Es gibt keine separate Canonical-Criteria-Schicht.
|
||
4. Jedes gelernte Source-Kriterium erhält eine stabile interne `CriterionDefinition.id` (UUID).
|
||
5. SearchFilter und Ergebnisattribute werden auf dieselbe `CriterionDefinition.id` gemappt.
|
||
6. INTERNAL-Kriterien werden nur ergänzt, wenn die Source relevante Eigenschaften nicht kennt.
|
||
7. URL-Encoding wird aus beobachtetem Source-Verhalten gelernt, nicht erfunden.
|
||
8. Nur verifizierte Mappings werden produktiv verwendet.
|
||
9. Taxonomy Learning und normale SearchRuns sind getrennte Prozesse.
|
||
10. Produktidentität bleibt konservativ.
|
||
11. Historie wird strukturiert gespeichert, kein HTML-Archiv.
|
||
12. Discovery, Ranking und Decision bleiben getrennt.
|
||
13. Vollständige Reconciliation darf nur auf vollständigen SearchRuns beruhen.
|
||
|
||
## 2.1 Konkret beobachtete Geizhals-Mechanismen für den Notebook-Pilot
|
||
|
||
Die aktuelle Notebook-Kategorie zeigt bereits ausreichend konkrete Muster, um den MVP-Pfad präziser festzulegen:
|
||
|
||
- zahlreiche Filter werden als auswählbare Optionen/Links dargestellt,
|
||
- Filtergruppen umfassen u. a. Hersteller, Diagonale, Auflösung, CPU-Hersteller, CPU-Serie, RAM, Speicher, Grafik, Betriebssystem und Gewicht,
|
||
- Preis, Gewicht und Diagonale sind typische Kandidaten für Range-/Input-Verhalten,
|
||
- die Ergebnisliste enthält bereits einen wesentlichen Teil der für den MVP relevanten Produktdaten, darunter Betriebssystem, Display, CPU, RAM, Speicher, Grafik, Modell-/Produktreferenz, Angebote und Bestpreis.
|
||
|
||
Diese Beobachtungen dienen als Pilotannahmen und müssen vor finaler Parser-Implementierung am echten Live-DOM validiert werden.
|
||
|
||
---
|
||
|
||
# 3. ProductCategory und SourceCategory
|
||
|
||
## 3.1 ProductCategory
|
||
|
||
Interner stabiler Katalog:
|
||
|
||
```text
|
||
ProductCategory
|
||
id
|
||
name
|
||
status
|
||
```
|
||
|
||
Beispiel:
|
||
|
||
```text
|
||
id = notebook
|
||
name = Notebooks
|
||
```
|
||
|
||
## 3.2 SourceCategory
|
||
|
||
Mapping zur Source:
|
||
|
||
```text
|
||
SourceCategory
|
||
id
|
||
source
|
||
external_category_key
|
||
product_category_id
|
||
source_url
|
||
status
|
||
taxonomy_status
|
||
last_taxonomy_scan_at
|
||
last_verified_at
|
||
```
|
||
|
||
Beispiel:
|
||
|
||
```text
|
||
source = geizhals
|
||
external_category_key = nb
|
||
product_category_id = notebook
|
||
```
|
||
|
||
---
|
||
|
||
# 4. SourceCategory Readiness
|
||
|
||
Eine SourceCategory besitzt einen technischen Readiness-Status:
|
||
|
||
```text
|
||
NOT_LEARNED
|
||
LEARNING
|
||
READY
|
||
DEGRADED
|
||
BROKEN
|
||
BLOCKED
|
||
```
|
||
|
||
## 4.1 READY
|
||
|
||
Eine SourceCategory ist `READY`, wenn mindestens:
|
||
|
||
- Baseline-Kategorieseite erreichbar ist
|
||
- Taxonomie technisch gelesen werden kann
|
||
- relevante Filterdefinitionen persistiert sind
|
||
- verwendbare URL-Encoding-Regeln verifiziert sind
|
||
- Query-Generation erfolgreich getestet wurde
|
||
- Result-Parsing mindestens für Basisdaten funktioniert
|
||
- kein aktiver Block-/Captcha-Zustand vorliegt
|
||
|
||
## 4.2 DEGRADED
|
||
|
||
`DEGRADED`, wenn:
|
||
|
||
- ein Teil der Filter nicht verifizierbar ist
|
||
- einzelne Mappings BROKEN sind
|
||
- Result-Parsing teilweise funktioniert
|
||
- Kategorie grundsätzlich weiter nutzbar ist
|
||
|
||
## 4.3 BROKEN
|
||
|
||
`BROKEN`, wenn Kernfunktionen nicht mehr zuverlässig arbeiten, z. B.:
|
||
|
||
- DOM-Struktur vollständig inkompatibel
|
||
- keine Query-Generierung mehr möglich
|
||
- Result Parser liefert keine verwertbaren Produkte
|
||
|
||
## 4.4 BLOCKED
|
||
|
||
`BLOCKED`, wenn Sourcezugriff aktuell durch:
|
||
|
||
- 403
|
||
- Captcha
|
||
- Challenge
|
||
- wiederholte 429
|
||
|
||
nicht weitergeführt werden soll.
|
||
|
||
---
|
||
|
||
# 5. Taxonomy Learning – operativer Algorithmus
|
||
|
||
Taxonomie wird **on demand** gelernt.
|
||
|
||
Der technische Ablauf ist verbindlich.
|
||
|
||
## 5.1 Schritt 1 – Baseline laden
|
||
|
||
Lade die Baseline-Kategorieseite, z. B.:
|
||
|
||
```text
|
||
https://geizhals.de/?cat=nb
|
||
```
|
||
|
||
Speichere:
|
||
|
||
```text
|
||
source_category
|
||
baseline_url
|
||
http_status
|
||
loaded_at
|
||
```
|
||
|
||
Keine HTML-Archivierung als Produktfunktion.
|
||
|
||
Fixtures dürfen für Tests aufgezeichnet werden.
|
||
|
||
## 5.2 Schritt 2 – Filtergruppen und Controls erkennen
|
||
|
||
Der Geizhals-Adapter analysiert die DOM-Struktur.
|
||
|
||
Zu erkennen sind, soweit vorhanden:
|
||
|
||
- Filtergruppen
|
||
- Filterlabel
|
||
- Checkboxen
|
||
- Radio-Optionen
|
||
- Dropdowns
|
||
- Multi-Selects
|
||
- Min/Max-Felder
|
||
- Range-Eingaben
|
||
- freie Texteingaben
|
||
- Links / hrefs
|
||
- optionale source-interne Filter-IDs
|
||
- Einheiten
|
||
- Wertebereiche
|
||
- sichtbare Optionslabels
|
||
|
||
Primärziel ist nicht die visuelle Darstellung, sondern die strukturelle Filtersemantik.
|
||
|
||
## 5.3 Schritt 3 – Filtermechanismus klassifizieren
|
||
|
||
Jeder erkannte Filter wird genau einem primären Lernweg zugeordnet:
|
||
|
||
### Typ A – Link-/href-basiert
|
||
|
||
Bevorzugter Weg.
|
||
|
||
Wenn Geizhals für eine Filteroption einen nutzbaren Link / href bereitstellt, wird dieser direkt ausgewertet.
|
||
|
||
Beispiel konzeptionell:
|
||
|
||
```html
|
||
<a href="?cat=nb&xf=17023_ab+24GB">ab 24 GB</a>
|
||
```
|
||
|
||
Der Adapter extrahiert:
|
||
|
||
```text
|
||
label
|
||
option_label
|
||
href
|
||
```
|
||
|
||
### Typ B – Range-/Input-Control
|
||
|
||
Für Controls wie Preis, Gewicht oder Diagonale, bei denen der Nutzer Werte eingibt oder einen Bereich setzt:
|
||
|
||
1. Baseline-URL erfassen.
|
||
2. einen kontrollierten Testwert setzen,
|
||
3. resultierende Navigation/Query-Änderung erfassen,
|
||
4. Query-Diff bestimmen,
|
||
5. Parametermuster persistieren,
|
||
6. mit einem zweiten Testwert prüfen, ob das Muster reproduzierbar ist.
|
||
|
||
### Typ C – JS-only Control
|
||
|
||
Nur wenn weder href noch statisch erkennbare Parameterstruktur vorhanden sind:
|
||
|
||
```text
|
||
Playwright-Fallback
|
||
```
|
||
|
||
Der Browser setzt den Filterzustand und beobachtet die resultierende URL bzw. Query.
|
||
|
||
Damit gilt die Priorität:
|
||
|
||
```text
|
||
A: href/link
|
||
→ B: Range/Input
|
||
→ C: Playwright-Fallback
|
||
```
|
||
|
||
## 5.4 Schritt 4 – Query-Diff bestimmen
|
||
|
||
Vergleiche Baseline-Query und Filter-Query.
|
||
|
||
Beispiel:
|
||
|
||
Baseline:
|
||
|
||
```text
|
||
?cat=nb
|
||
```
|
||
|
||
Filterzustand:
|
||
|
||
```text
|
||
?cat=nb&xf=17023_ab+24GB
|
||
```
|
||
|
||
Query-Diff:
|
||
|
||
```text
|
||
parameter = xf
|
||
value = 17023_ab+24GB
|
||
```
|
||
|
||
Für Preis können z. B. getrennte Parameter wie `bpmin` / `bpmax` erkannt werden.
|
||
|
||
## 5.5 Schritt 5 – Filterdefinition speichern
|
||
|
||
Erzeuge / aktualisiere:
|
||
|
||
```text
|
||
SourceFilterDefinition
|
||
```
|
||
|
||
Mindestens:
|
||
|
||
```text
|
||
id
|
||
source_category_id
|
||
criterion_definition_id
|
||
external_filter_key nullable
|
||
label
|
||
filter_type
|
||
unit nullable
|
||
encoding_type
|
||
status
|
||
last_seen_at
|
||
last_verified_at
|
||
```
|
||
|
||
Filtertypen:
|
||
|
||
```text
|
||
BOOLEAN
|
||
ENUM
|
||
MULTI_ENUM
|
||
NUMERIC_RANGE
|
||
NUMERIC_THRESHOLD
|
||
TEXT
|
||
UNKNOWN
|
||
```
|
||
|
||
Encoding-Typen mindestens:
|
||
|
||
```text
|
||
OPTION_TOKEN
|
||
RANGE_PARAMETERS
|
||
BOOLEAN_PARAMETER
|
||
TEXT_PARAMETER
|
||
UNKNOWN
|
||
```
|
||
|
||
---
|
||
|
||
# 6. SourceFilterOption / SourceFilterRange
|
||
|
||
## 6.1 Diskrete Optionen
|
||
|
||
```text
|
||
SourceFilterOption
|
||
- id
|
||
- source_filter_definition_id
|
||
- source_label
|
||
- normalized_value nullable
|
||
- operator nullable
|
||
- unit nullable
|
||
- query_parameter
|
||
- query_value
|
||
- status
|
||
```
|
||
|
||
## 6.2 Range-Filter
|
||
|
||
```text
|
||
SourceFilterRange
|
||
- source_filter_definition_id
|
||
- min_parameter nullable
|
||
- max_parameter nullable
|
||
- minimum nullable
|
||
- maximum nullable
|
||
- step nullable
|
||
- unit nullable
|
||
```
|
||
|
||
---
|
||
|
||
# 7. Playwright-Fallback für JS-only Controls
|
||
|
||
Wenn ein Filter kein direkt verwertbares href bzw. keine statisch erkennbare Query-Struktur liefert:
|
||
|
||
```text
|
||
Taxonomy Learning
|
||
↓
|
||
Playwright
|
||
```
|
||
|
||
Ablauf:
|
||
|
||
1. Kategorieseite öffnen
|
||
2. Ziel-Filter lokalisieren
|
||
3. Filterwert setzen
|
||
4. Navigation / URL-Änderung abwarten
|
||
5. resultierende URL erfassen
|
||
6. Query-Diff zur Baseline berechnen
|
||
7. Filterzustand wieder zurücksetzen
|
||
|
||
Wichtig:
|
||
|
||
- Playwright ist Fallback, nicht Primärmechanismus.
|
||
- Playwright wird für Taxonomy Learning verwendet, nicht für jeden normalen SearchRun.
|
||
- Kein Captcha-Bypass.
|
||
- Kein Login-Bypass.
|
||
- Kein Umgehen technischer Schutzmechanismen.
|
||
|
||
---
|
||
|
||
# 8. Mapping Lifecycle
|
||
|
||
Filter-/Encoding-Mappings besitzen Status:
|
||
|
||
```text
|
||
DISCOVERED
|
||
LEARNED_UNVERIFIED
|
||
VERIFIED
|
||
BROKEN
|
||
DISABLED
|
||
```
|
||
|
||
Nur `VERIFIED` wird für produktive Query-Generierung verwendet.
|
||
|
||
---
|
||
|
||
# 9. Roundtrip-Verifikation
|
||
|
||
Die Roundtrip-Verifikation ist verbindlich.
|
||
|
||
Beispiel:
|
||
|
||
```text
|
||
RAM >= 24 GB
|
||
↓
|
||
gelernte Query-Regel
|
||
↓
|
||
URL selbst generieren
|
||
↓
|
||
Geizhals aufrufen
|
||
↓
|
||
aktiven Filterzustand prüfen
|
||
```
|
||
|
||
Das Mapping wird nur `VERIFIED`, wenn der Adapter auf der Zielseite eindeutig erkennt, dass der erwartete Filter aktiv ist.
|
||
|
||
Wenn Verifikation fehlschlägt:
|
||
|
||
```text
|
||
LEARNED_UNVERIFIED
|
||
```
|
||
|
||
Wenn vorher VERIFIED und nun nicht mehr reproduzierbar:
|
||
|
||
```text
|
||
BROKEN
|
||
```
|
||
|
||
---
|
||
|
||
# 10. Erkennung von Source-Änderungen
|
||
|
||
Taxonomy Learning vergleicht:
|
||
|
||
```text
|
||
vorheriger Filterbestand
|
||
vs.
|
||
aktueller Filterbestand
|
||
```
|
||
|
||
Zu erkennen sind mindestens:
|
||
|
||
- Filter neu
|
||
- Filter verschwunden
|
||
- Filterlabel geändert
|
||
- externe Filter-ID geändert
|
||
- Optionen geändert
|
||
- Encoding geändert
|
||
- Unit geändert
|
||
- Control-Typ geändert
|
||
|
||
Bei relevanter Änderung:
|
||
|
||
```text
|
||
mapping_version++
|
||
```
|
||
|
||
Betroffene DiscoverySearches werden:
|
||
|
||
```text
|
||
OUT_OF_SYNC
|
||
```
|
||
|
||
wenn ihre Query auf geänderten Mappings basiert.
|
||
|
||
---
|
||
|
||
# 11. CriterionDefinition
|
||
|
||
Jedes Source-Kriterium erhält eine stabile interne ID:
|
||
|
||
```text
|
||
CriterionDefinition
|
||
- id UUID
|
||
- origin SOURCE | INTERNAL
|
||
- product_category_id
|
||
- source_id nullable
|
||
- source_category_id nullable
|
||
- label
|
||
- value_type
|
||
- unit nullable
|
||
- scoring_attribute_key nullable
|
||
- status
|
||
```
|
||
|
||
`CriterionDefinition.id` ist Referenzidentität, keine globale Ontologie.
|
||
|
||
---
|
||
|
||
# 12. SourceAttributeMapping
|
||
|
||
Suchfilter und Produktattribute müssen verbunden werden.
|
||
|
||
Dafür:
|
||
|
||
```text
|
||
SourceAttributeMapping
|
||
- id
|
||
- source_category_id
|
||
- source_attribute_key nullable
|
||
- source_attribute_label
|
||
- criterion_definition_id
|
||
- normalizer_id nullable
|
||
- status
|
||
```
|
||
|
||
Matching-Priorität:
|
||
|
||
1. stabile Source-Attribute-ID
|
||
2. bekannte SourceAttributeMapping-Regel
|
||
3. normalisiertes Label-Matching
|
||
4. ansonsten UNMAPPED
|
||
|
||
Keine aggressive automatische Zuordnung bei Unsicherheit.
|
||
|
||
---
|
||
|
||
# 13. available-criteria
|
||
|
||
Endpoint:
|
||
|
||
```text
|
||
GET /api/v1/product-categories/{product_category_id}/available-criteria?source=geizhals
|
||
```
|
||
|
||
Nur verwendbare Kriterien werden dem Standardnutzer als Discovery-Filter angeboten.
|
||
|
||
Mindestens:
|
||
|
||
```text
|
||
criterion_definition_id
|
||
label
|
||
filter_type
|
||
unit
|
||
operators
|
||
options / range
|
||
mapping_status
|
||
supports_discovery
|
||
```
|
||
|
||
---
|
||
|
||
# 14. Query Generation
|
||
|
||
Die Source-URL wird ausschließlich aus:
|
||
|
||
```text
|
||
SourceCategory
|
||
+
|
||
aktuelle ProjectCriteria
|
||
+
|
||
VERIFIED SourceFilterMappings
|
||
```
|
||
|
||
erzeugt.
|
||
|
||
Keine URL-Templates aus LLM.
|
||
|
||
Keine hartcodierten notebook-spezifischen Query-Fragmente außerhalb des Geizhals-Adapters / Source-Datenmodells.
|
||
|
||
Die Query-Generierung muss deterministisch, reproduzierbar, testbar und versionsgebunden sein.
|
||
|
||
---
|
||
|
||
# 15. SearchRun
|
||
|
||
Ein SearchRun hält mindestens:
|
||
|
||
```text
|
||
discovery_search_id
|
||
criteria_version
|
||
mapping_version
|
||
generated_url
|
||
started_at
|
||
finished_at
|
||
status
|
||
result_count
|
||
page_count
|
||
new_count
|
||
changed_count
|
||
unchanged_count
|
||
missing_count
|
||
```
|
||
|
||
---
|
||
|
||
# 16. Ergebnisabruf und Pagination
|
||
|
||
Ein Full SearchRun versucht grundsätzlich die vollständige Ergebnismenge zu lesen.
|
||
|
||
Wenn nicht alle Seiten vollständig gelesen werden:
|
||
|
||
```text
|
||
SearchRun.status = PARTIAL
|
||
```
|
||
|
||
Ein `PARTIAL`-Run darf keine:
|
||
|
||
```text
|
||
MISSING
|
||
DISAPPEARED
|
||
```
|
||
|
||
Transitions erzeugen.
|
||
|
||
---
|
||
|
||
# 17. TOO_BROAD
|
||
|
||
Eine DiscoverySearch darf einen konfigurierbaren Schwellenwert besitzen.
|
||
|
||
Beispiel:
|
||
|
||
```text
|
||
broad_result_threshold = 300
|
||
```
|
||
|
||
Bei Überschreitung:
|
||
|
||
```text
|
||
SearchRun.status = TOO_BROAD
|
||
```
|
||
|
||
Kein stilles Top-N-Limit.
|
||
|
||
Ein technischer Safety-Cap ist zulässig, wenn er explizit konfiguriert und sichtbar dokumentiert ist; der Lauf gilt dann nicht als Full Run.
|
||
|
||
---
|
||
|
||
# 18. Zweistufige Datenerfassung
|
||
|
||
Die Suchergebnisliste ist die **primäre Datenquelle des MVP**.
|
||
|
||
Der Notebook-Pilot muss zunächst möglichst viel aus der Ergebnisliste gewinnen, bevor zusätzliche Produktdetailseiten geladen werden.
|
||
|
||
## 18.1 Stufe A – Suchergebnisliste
|
||
|
||
Jeder SearchRun liest aus der Ergebnisliste mindestens:
|
||
|
||
```text
|
||
source_product_url
|
||
title
|
||
manufacturer/model soweit sichtbar
|
||
best_price
|
||
currency
|
||
availability soweit sichtbar
|
||
seller_count soweit sichtbar
|
||
sichtbare Attribute
|
||
```
|
||
|
||
Für die Notebook-Kategorie sind insbesondere folgende sichtbare Basisattribute als realistische MVP-Felder zu validieren:
|
||
|
||
```text
|
||
Betriebssystem
|
||
Display
|
||
CPU
|
||
RAM
|
||
Speicher
|
||
Grafik
|
||
Modell-/Produktreferenz
|
||
Angebote
|
||
Bestpreis
|
||
```
|
||
|
||
Die genaue DOM-Struktur und die Verfügbarkeit einzelner Felder dürfen nicht vorab erfunden werden; sie werden im technischen Spike und Live-Pilot bestätigt.
|
||
|
||
## 18.2 Stufe B – Produktdetailseite
|
||
|
||
Produktdetailseiten werden nicht blind für alle Treffer geladen.
|
||
|
||
Sie werden für relevante Kandidaten geladen, insbesondere:
|
||
|
||
- Shortlist
|
||
- Top-N nach ProductScore
|
||
- Kandidaten mit fehlenden für Ranking relevanten Attributen
|
||
- Kandidaten mit unsicherer Identität, wenn Detailseite helfen kann
|
||
|
||
Der Notebook-Pilot muss praktisch feststellen, welche Daten auf Liste und Detailseite verfügbar sind.
|
||
|
||
---
|
||
|
||
# 19. SourceProductReference
|
||
|
||
Mindestens:
|
||
|
||
```text
|
||
id
|
||
source_id
|
||
source_category_id
|
||
source_url
|
||
source_title
|
||
manufacturer nullable
|
||
model nullable
|
||
manufacturer_sku nullable
|
||
gtin_ean nullable
|
||
normalized_spec_fingerprint nullable
|
||
match_status
|
||
lifecycle_status
|
||
first_seen_at
|
||
last_seen_at
|
||
```
|
||
|
||
---
|
||
|
||
# 20. Produktidentität – konservativer Pilot
|
||
|
||
Signalreihenfolge:
|
||
|
||
1. stabile Source-ID, falls entdeckt
|
||
2. GTIN/EAN
|
||
3. Hersteller-SKU
|
||
4. kanonische Source-URL
|
||
5. normalisierter Titel
|
||
6. Hersteller + Modell
|
||
7. differenzierende Specs
|
||
|
||
Bei Unsicherheit:
|
||
|
||
```text
|
||
MATCH_UNCERTAIN
|
||
```
|
||
|
||
Kein automatisches Merge.
|
||
|
||
---
|
||
|
||
# 21. ProductObservation
|
||
|
||
```text
|
||
ProductObservation
|
||
- id
|
||
- source_product_reference_id
|
||
- search_run_id
|
||
- observed_at
|
||
- raw_title
|
||
- best_price
|
||
- currency
|
||
- availability nullable
|
||
- seller_count nullable
|
||
```
|
||
|
||
---
|
||
|
||
# 22. ProductAttributeObservation
|
||
|
||
```text
|
||
ProductAttributeObservation
|
||
- product_observation_id
|
||
- criterion_definition_id nullable
|
||
- raw_name
|
||
- raw_value
|
||
- normalized_value nullable
|
||
- unit nullable
|
||
- mapping_status
|
||
```
|
||
|
||
Wenn kein Mapping existiert:
|
||
|
||
```text
|
||
criterion_definition_id = null
|
||
mapping_status = UNMAPPED
|
||
```
|
||
|
||
Unmapped Felder dürfen für spätere Admin-Korrektur sichtbar bleiben.
|
||
|
||
---
|
||
|
||
# 23. Normalisierung
|
||
|
||
Normalizer arbeitet kriteriumsabhängig.
|
||
|
||
Beispiele:
|
||
|
||
```text
|
||
"32 GB" -> 32 / GB
|
||
"1 TB" -> 1024 / GB, falls Zielunit GB
|
||
"1,45 kg" -> 1.45 / kg
|
||
```
|
||
|
||
Zu speichern:
|
||
|
||
```text
|
||
raw_value
|
||
normalized_value
|
||
unit
|
||
normalizer_version
|
||
```
|
||
|
||
---
|
||
|
||
# 24. Diffing
|
||
|
||
Verglichen werden nur vollständige Runs derselben DiscoverySearch.
|
||
|
||
Run Diff:
|
||
|
||
```text
|
||
NEW
|
||
CHANGED
|
||
UNCHANGED
|
||
MISSING
|
||
```
|
||
|
||
CHANGED mindestens bei relevanter Änderung von:
|
||
|
||
```text
|
||
best_price
|
||
currency
|
||
availability
|
||
seller_count
|
||
normalisierte Attribute
|
||
```
|
||
|
||
---
|
||
|
||
# 25. Product Lifecycle
|
||
|
||
```text
|
||
ACTIVE
|
||
DISAPPEARED
|
||
ARCHIVED
|
||
```
|
||
|
||
Regeln:
|
||
|
||
```text
|
||
1 Full Run fehlend -> Run Diff MISSING
|
||
3 aufeinanderfolgende Full Runs fehlend -> Lifecycle DISAPPEARED
|
||
```
|
||
|
||
---
|
||
|
||
# 26. Ranking und Shortlist
|
||
|
||
Bestehende Scoring-Engine kann über:
|
||
|
||
```text
|
||
CriterionDefinition.scoring_attribute_key
|
||
```
|
||
|
||
angesprochen werden.
|
||
|
||
Ohne Ranking-Konfiguration:
|
||
|
||
```text
|
||
ranking_status = NOT_CONFIGURED
|
||
```
|
||
|
||
Mit Ranking:
|
||
|
||
```text
|
||
ProductScore
|
||
product_eligibility
|
||
```
|
||
|
||
Shortlist Default: `5`
|
||
|
||
---
|
||
|
||
# 27. Scheduling und Backoff
|
||
|
||
Default:
|
||
|
||
```text
|
||
180 Minuten
|
||
```
|
||
|
||
auf `DiscoverySearch`.
|
||
|
||
Defensives Verhalten:
|
||
|
||
- Timeout
|
||
- Retry mit exponentiellem Backoff
|
||
- Jitter
|
||
- 429 -> BACKOFF
|
||
- 403 -> BLOCKED
|
||
- Captcha / Challenge -> BLOCKED
|
||
- wiederholte Parsing-Fehler -> DEGRADED/BROKEN
|
||
|
||
Keine Schutzumgehung.
|
||
|
||
---
|
||
|
||
# 28. UI-Reaktion auf SourceCategory Readiness
|
||
|
||
## READY
|
||
Normale Kriterienauswahl und Suchanlage.
|
||
|
||
## NOT_LEARNED
|
||
UI zeigt: „Kriterien für diese Produktkategorie werden gerade vorbereitet.“
|
||
|
||
## LEARNING
|
||
Loading-/Progress-Zustand.
|
||
|
||
## DEGRADED
|
||
VERIFIED Kriterien dürfen angeboten werden; Warnung anzeigen.
|
||
|
||
## BROKEN
|
||
Keine neue produktive Suche.
|
||
|
||
## BLOCKED
|
||
Quelle derzeit nicht automatisiert abfragbar.
|
||
|
||
---
|
||
|
||
# 29. Taxonomy Learning API
|
||
|
||
Mindestens:
|
||
|
||
```text
|
||
POST /api/v1/sources/{source}/categories/{category}/discover-taxonomy
|
||
GET /api/v1/sources/{source}/categories/{category}/taxonomy-status
|
||
GET /api/v1/sources/{source}/categories/{category}/filters
|
||
```
|
||
|
||
Response mindestens:
|
||
|
||
```text
|
||
source_category_status
|
||
filters_discovered
|
||
filters_verified
|
||
filters_unverified
|
||
filters_broken
|
||
mapping_version
|
||
last_scan_at
|
||
```
|
||
|
||
---
|
||
|
||
# 30. Source Adapter Contract
|
||
|
||
```python
|
||
class DiscoverySourceAdapter(Protocol):
|
||
def load_category_baseline(category_ref): ...
|
||
def discover_filter_controls(document): ...
|
||
def extract_filter_links(document): ...
|
||
def derive_query_diff(baseline_url, filtered_url): ...
|
||
def verify_filter_roundtrip(filter_definition, encoded_query): ...
|
||
def build_query(criteria, mappings): ...
|
||
def fetch_result_page(url): ...
|
||
def parse_result_page(document): ...
|
||
def parse_product_detail(document): ...
|
||
def extract_active_filters(document): ...
|
||
def normalize_source_attribute(raw_attribute): ...
|
||
```
|
||
|
||
Playwright darf intern als Fallback-Service genutzt werden.
|
||
|
||
---
|
||
|
||
# 31. Technische Module
|
||
|
||
Mindestens logisch getrennt:
|
||
|
||
```text
|
||
sources/geizhals/
|
||
category_loader
|
||
taxonomy_parser
|
||
filter_encoder
|
||
roundtrip_verifier
|
||
result_parser
|
||
product_detail_parser
|
||
|
||
discovery/
|
||
taxonomy_service
|
||
criterion_service
|
||
mapping_service
|
||
search_execution_service
|
||
identity_service
|
||
observation_service
|
||
diff_service
|
||
candidate_service
|
||
shortlist_service
|
||
```
|
||
|
||
Keine Geizhals-DOM-Logik außerhalb `sources/geizhals`.
|
||
|
||
---
|
||
|
||
# 32. Verpflichtender technischer Geizhals-Spike vor finalem Parserbau
|
||
|
||
Vor der finalen Implementierung der Geizhals-Parser muss der Coding-Agent die echte Notebook-Seite technisch inspizieren.
|
||
|
||
Der Spike muss dokumentieren:
|
||
|
||
1. stabile DOM-Anker bzw. semantisch belastbare Selektoren,
|
||
2. tatsächliche Struktur der Filtergruppen,
|
||
3. Klassifikation der Filter in Typ A/B/C,
|
||
4. konkrete href-/Query-Muster mehrerer Filter,
|
||
5. tatsächliches Verhalten von Preis, Gewicht und Diagonale,
|
||
6. Pagination-Struktur,
|
||
7. Produktkarten-/Ergebnisstruktur,
|
||
8. verfügbare Felder direkt in der Ergebnisliste,
|
||
9. Unterschiede zwischen Listen- und Produktdetaildaten,
|
||
10. auffällige dynamische/JS-geladene Bereiche.
|
||
|
||
Wichtig:
|
||
|
||
- keine CSS-Klassen, DOM-Pfade oder Selektoren vorab erfinden,
|
||
- bevorzugt stabile semantische Attribute oder Source-eigene IDs verwenden,
|
||
- fragile rein visuelle Selektoren nur, wenn keine robustere Alternative existiert,
|
||
- die Ergebnisse des Spikes müssen unmittelbar in Fixtures und Parser-Tests überführt werden.
|
||
|
||
Der Spike ist Teil der MVP-Umsetzung, nicht optionale Vorarbeit.
|
||
|
||
---
|
||
|
||
# 34. Fixtures
|
||
|
||
Fixtures sind für reproduzierbare Tests Pflicht.
|
||
|
||
Mindestens:
|
||
|
||
- Notebook Baseline Category HTML
|
||
- Kategorie mit gesetztem RAM-Filter
|
||
- Kategorie mit Preisbereich
|
||
- Ergebnisliste Seite 1
|
||
- Ergebnisliste Seite 2
|
||
- Produktdetailseite
|
||
- geänderte DOM-Version / broken fixture
|
||
- Captcha/Challenge fixture, soweit technisch sinnvoll
|
||
- 429/403 als HTTP-Testzustände
|
||
|
||
---
|
||
|
||
# 34. Unit Tests
|
||
|
||
Mindestens:
|
||
|
||
## Taxonomy
|
||
- Filtergruppen erkannt
|
||
- Filterlabel erkannt
|
||
- href extrahiert
|
||
- Query-Diff korrekt
|
||
- Filteroption gespeichert
|
||
- Range-Parameter erkannt
|
||
- JS-only Control nutzt Fallback-Pfad
|
||
- Mapping Lifecycle korrekt
|
||
- Roundtrip -> VERIFIED
|
||
- Roundtrip fail -> LEARNED_UNVERIFIED
|
||
- ehemals VERIFIED + fail -> BROKEN
|
||
|
||
## Attribute Mapping
|
||
- RAM -> richtige CriterionDefinition
|
||
- unbekanntes Label -> UNMAPPED
|
||
- Source-ID hat Vorrang vor Label
|
||
- Unit-Normalisierung korrekt
|
||
|
||
## Query Generation
|
||
- nur VERIFIED
|
||
- deterministisch
|
||
- criteria_version/mapping_version gebunden
|
||
- INTERNAL ohne Source-Mapping beeinflusst URL nicht
|
||
|
||
---
|
||
|
||
# 35. Integration Tests
|
||
|
||
Mindestens:
|
||
|
||
```text
|
||
Taxonomy Fixture
|
||
→ DB
|
||
→ available-criteria
|
||
→ ProjectCriteria
|
||
→ Query Generation
|
||
→ SearchRun Fixture
|
||
→ Result Parse
|
||
→ SourceProductReference
|
||
→ ProductObservation
|
||
→ ProductAttributeObservation
|
||
→ Diff
|
||
→ Candidate DTO
|
||
```
|
||
|
||
---
|
||
|
||
# 36. Manueller Live-Notebook-Pilot
|
||
|
||
Der MVP gilt im Source-/Discovery-Bereich **nicht als abgenommen**, solange kein dokumentierter Live-Pilot gegen die echte Geizhals-Notebook-Kategorie erfolgreich durchgeführt wurde.
|
||
|
||
Der Pilot muss mindestens zeigen:
|
||
|
||
1. Notebook-Kategorie wird erreichbar geladen.
|
||
2. reale Filter werden erkannt.
|
||
3. mehrere unterschiedliche Filtertypen werden gelernt.
|
||
4. RAM-Filter wird technisch korrekt gelernt.
|
||
5. Preisfilter wird technisch korrekt gelernt.
|
||
6. Roundtrip-Verifikation funktioniert.
|
||
7. `available-criteria` liefert reale Filter.
|
||
8. daraus wird eine reale Such-URL generiert.
|
||
9. reale Suchergebnisse werden gelesen.
|
||
10. mindestens Titel + Produktlink + Preis werden extrahiert.
|
||
11. sichtbare Produktattribute werden gemappt.
|
||
12. mindestens zwei SearchRuns erzeugen History/Diff.
|
||
13. UI kann daraus Kandidaten anzeigen.
|
||
|
||
Der Live-Pilot muss mindestens einen vollständigen technischen Kettennachweis enthalten:
|
||
|
||
```text
|
||
echte Filtergruppe erkannt
|
||
→ href/Input-Verhalten ermittelt
|
||
→ Query-Regel gelernt
|
||
→ Roundtrip verifiziert
|
||
→ reale Ergebnisse geparst
|
||
→ reale Attribute auf CriterionDefinition gemappt
|
||
```
|
||
|
||
Live-Pilot wird manuell/dokumentiert ausgeführt, nicht in CI.
|
||
|
||
---
|
||
|
||
# 37. Fehlerfälle
|
||
|
||
## DOM geändert
|
||
`DEGRADED` oder `BROKEN`
|
||
|
||
## Filter nicht verifizierbar
|
||
`LEARNED_UNVERIFIED`
|
||
|
||
## URL-Encoding geändert
|
||
früher VERIFIED, Roundtrip scheitert:
|
||
|
||
```text
|
||
BROKEN
|
||
mapping_version++
|
||
DiscoverySearch -> OUT_OF_SYNC
|
||
```
|
||
|
||
## Result Parser unvollständig
|
||
teilweise Daten -> `DEGRADED`
|
||
keine verwertbaren Produkte -> `BROKEN`
|
||
|
||
## Source blockiert
|
||
`BLOCKED`
|
||
|
||
## Taxonomie teilweise verfügbar
|
||
VERIFIED Kriterien dürfen weiter angeboten werden.
|
||
|
||
---
|
||
|
||
# 38. Kein stiller Fallback
|
||
|
||
Nicht erlaubt:
|
||
|
||
- Filter-Encoding raten
|
||
- unbekannte Source-Attribute automatisch mergen
|
||
- unvollständigen Run als vollständigen Run behandeln
|
||
- BROKEN Mapping weiterverwenden
|
||
- fehlende Kriterien durch frei erfundene Defaults ersetzen
|
||
- Candidate Ranking mit nicht belegten Attributen auffüllen
|
||
|
||
---
|
||
|
||
# 39. Definition of Done
|
||
|
||
## Taxonomy / URL Learning
|
||
- [ ] Kategorieseite wird real gelesen.
|
||
- [ ] Filter werden aus dem DOM erkannt.
|
||
- [ ] href-basierte Filter werden automatisch gelernt.
|
||
- [ ] Query-Diff funktioniert.
|
||
- [ ] Range-Parameter funktionieren.
|
||
- [ ] Playwright-Fallback existiert für JS-only Controls.
|
||
- [ ] Roundtrip-Verifikation existiert.
|
||
- [ ] Mapping Lifecycle funktioniert.
|
||
- [ ] Geizhals-Strukturänderungen werden erkannt.
|
||
- [ ] SourceCategory Readiness funktioniert.
|
||
|
||
## Criteria / Mapping
|
||
- [ ] Source-Kriterien erhalten stabile CriterionDefinition-UUIDs.
|
||
- [ ] SearchFilter referenziert CriterionDefinition.
|
||
- [ ] SourceAttributeMapping referenziert dieselbe CriterionDefinition.
|
||
- [ ] UNMAPPED Attribute bleiben sichtbar.
|
||
- [ ] INTERNAL-Kriterien möglich.
|
||
- [ ] available-criteria basiert auf real gelernter Taxonomie.
|
||
|
||
## Search / Results
|
||
- [ ] Query-Generierung nutzt nur VERIFIED Mappings.
|
||
- [ ] Full Pagination funktioniert.
|
||
- [ ] PARTIAL wird korrekt erkannt.
|
||
- [ ] PARTIAL löst kein MISSING/DISAPPEARED aus.
|
||
- [ ] Basisdaten aus Ergebnisliste werden extrahiert.
|
||
- [ ] Produktdetailseite kann gezielt angereichert werden.
|
||
- [ ] SourceProductReference funktioniert.
|
||
- [ ] ProductObservation funktioniert.
|
||
- [ ] ProductAttributeObservation funktioniert.
|
||
- [ ] Attribute werden normalisiert/historisiert.
|
||
|
||
## Diff / Candidate
|
||
- [ ] NEW / CHANGED / UNCHANGED / MISSING funktionieren.
|
||
- [ ] DISAPPEARED nach drei Full Runs.
|
||
- [ ] Candidate DTOs werden erzeugt.
|
||
- [ ] Ranking nutzt nur vorhandene gemappte Daten.
|
||
- [ ] Shortlist funktioniert, wenn Ranking konfiguriert.
|
||
|
||
## MVP Acceptance
|
||
- [ ] Notebook Live-Pilot vollständig dokumentiert.
|
||
- [ ] echte Geizhals-Kriterien im UI sichtbar.
|
||
- [ ] echte Such-URL aus UI-Kriterien generiert.
|
||
- [ ] echte Resultate eingelesen.
|
||
- [ ] echte Preise/Attribute angezeigt.
|
||
- [ ] History/Diff über mindestens zwei Runs sichtbar.
|
||
- [ ] Source-/Discovery-Pfad end-to-end über UI nutzbar.
|
||
|
||
---
|
||
|
||
# 40. Arbeitsauftrag an den Coding-Agenten
|
||
|
||
Diese Datei ersetzt `buying-agent-source-discovery-spec-v0.2.md` vollständig.
|
||
|
||
Bitte **noch nicht implementieren**.
|
||
|
||
Prüfe erneut den aktuellen Repository-Stand gegen:
|
||
|
||
1. `buying-agent-v0.1-spec-v1.1.md`
|
||
2. `buying-agent-architecture-addendum-v1.md`
|
||
3. `buying-agent-source-discovery-spec-v0.3.md`
|
||
4. `buying-agent-frontend-mvp-spec-v0.3.md`
|
||
|
||
Prüfe insbesondere:
|
||
|
||
- ob der Taxonomy-Learning-Algorithmus vollständig implementierbar beschrieben ist,
|
||
- ob DOM/href-basierte Discovery ausreicht bzw. welche konkreten Geizhals-Strukturen im Live-Pilot validiert werden müssen,
|
||
- ob Playwright-Fallback sauber begrenzt ist,
|
||
- ob Query-Diff + Roundtrip-Verifikation widerspruchsfrei sind,
|
||
- ob SourceFilterMapping und SourceAttributeMapping klar getrennt sind,
|
||
- ob dieselbe CriterionDefinition-ID Search und Result Attributes verbindet,
|
||
- ob Readiness-Status ausreichend definiert sind,
|
||
- ob zweistufige Datenerfassung (Liste/Detailseite) implementierbar ist,
|
||
- ob Full/PARTIAL-Semantik vollständig ist,
|
||
- ob der Notebook Live-Pilot als echte MVP-Abnahme genügt,
|
||
- ob der technische Geizhals-Spike vor finalem Parserbau ausreichend konkret beschrieben ist,
|
||
- ob stabile DOM-Anker und Pagination im Live-DOM ohne erfundene Selektoren verifiziert werden können,
|
||
- ob Ergebnislisten-Daten für den MVP tatsächlich ausreichen und welche Felder zwingend Detailseiten benötigen.
|
||
|
||
Berichte ausschließlich:
|
||
|
||
- verbleibende echte fachliche Widersprüche,
|
||
- fehlende technische Verträge,
|
||
- technisch riskante Annahmen,
|
||
- notwendige Backend-Änderungen,
|
||
- notwendige Frontend-Änderungen.
|
||
|
||
Keine neue Fachlogik erfinden.
|
||
|
||
Wenn keine blockierenden Punkte mehr bestehen:
|
||
|
||
1. aktualisierten vertikalen Implementierungsplan erstellen,
|
||
2. jeder Slice muss über die UI prüfbar sein,
|
||
3. Taxonomy Learning + Query Generation + Result Parsing früh implementieren,
|
||
4. Live-Notebook-Pilot vor Abschluss durchführen,
|
||
5. Implementierung erst nach ausdrücklicher Freigabe des Reviews starten.
|