BuyingAgent/.specs/buying-agent-v0.1-spec.md
Lars 3ac42645ef
All checks were successful
Deploy Development / deploy (push) Successful in 1m6s
Test Suite / smoke-dev (push) Successful in 0s
Richte Develop-Build und Main-Deploy analog zur Jinkendo-Familie ein.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 10:47:48 +02:00

49 KiB
Raw Blame History

Buying Agent v0.1 – Implementierungsspezifikation

Status: implementierungsbereit · Spezifikationsversion: 1.1 · Stand: 2026-10-03

Änderungen gegenüber Spezifikationsversion 1.0

  • Preference-Scoring erhält ein explizites strategy-Feld; v0.1 implementiert ausschließlich numeric_linear, ohne vorsorgliche Plugin-Infrastruktur.
  • Produkt-Eignung und Angebots-Eignung werden getrennt als product_eligibility und offer_eligibility bewertet; overall_eligibility wird daraus abgeleitet.
  • Die Preisbewertung wird konzeptionell von der Identität eines Offers entkoppelt: v0.1 nutzt weiterhin nur die Historie des konkreten Offers, spätere Scoring-Versionen dürfen jedoch Variant- oder Market-Level-Referenzserien konsumieren.
  • raw_value wird bei JSONB als inhaltlich unverändert, nicht als bytegenau identisch beschrieben.
  • Ein verbindlicher Future Extension Contract dokumentiert, welche späteren Erweiterungen v0.1 nicht verbauen darf, ohne sie bereits zu implementieren.

1. Auftrag und Zielbild

Implementiere ein eigenständig lauffähiges Backend für einen generischen Buying-/Deal-Agenten. Das System bewertet, wie gut eine konkrete Produktvariante zu einem Bedarfsprofil passt und wie attraktiv ein bestimmtes Angebot preislich ist. Es liefert nachvollziehbare Entscheidungen, keine automatischen Käufe.

Notebooks sind der erste Anwendungsfall, nicht das Datenmodell. Weitere Kategorien wie GPU-Server, Monitore oder Kameras müssen durch neue Attribute und Profile ohne Änderungen an der Engine oder zusätzliche produktspezifische DB-Spalten möglich sein.

Diese Datei ist die vollständige Arbeitsgrundlage. Ein Zugriff auf den ursprünglichen Chat ist nicht erforderlich. Die dort beschlossene Architektur wird beibehalten. Die hier definierten Formeln, Fehlerfälle, API-Verträge und Zeitregeln konkretisieren bislang offene Punkte als verbindliche v0.1-Entscheidungen. Die Beispielwerte sind synthetische Testdaten und keine Markt- oder Herstellerangaben.

Walking Skeleton

Der MVP muss diesen vollständigen Ablauf tatsächlich ausführen und dauerhaft speichern:

Profil → Produkt → Variante + Attribute → Angebot → Preisbeobachtungen
       → Constraints → ProductScore → PriceScore → DealScore
       → Klassifikation + Begründungen → gespeicherte Evaluation → API-Antwort

Ein manuell erfasster Kandidat genügt für den ersten vertikalen Durchstich. Danach werden Grenzfälle und drei Beispielkandidaten ergänzt. Keine bloßen Interfaces ohne funktionsfähigen End-to-End-Pfad.

Explizit nicht enthalten

  • Keepa, Idealo, Geizhals oder andere externe Datenquellen und Scraper.
  • n8n, Scheduler, Benachrichtigungen und Hintergrundjobs.
  • LLM-Normalisierung oder LLM-Bewertung.
  • Web-UI; generierte OpenAPI-/Swagger-Dokumentation ist erlaubt.
  • Auto-Discovery, automatische Produktsuche oder automatische Zuordnung fremder Produkte.
  • Checkout, automatische Käufe, Nutzerkonten, Mehrmandantenfähigkeit und öffentliche Bereitstellung.
  • Wechselkursumrechnung, marktweite Preisprognosen, ML und Optimierung von Gewichten.

Diese Funktionen dürfen später über Adapter und Services ergänzt werden. Keine vorsorglichen Frameworks, leeren Integrationen oder Infrastruktur dafür bauen.

2. Stack und Architekturgrenzen

Verbindlich: Python 3.13, FastAPI, Pydantic 2, SQLAlchemy 2, Alembic, PostgreSQL 17, pytest und Docker Compose. Verwende einen synchronen SQLAlchemy-Zugriff mit psycopg 3; ein asynchroner Stack ist für v0.1 nicht erforderlich. Abhängigkeiten mit kompatiblen Versionen und einem eingecheckten Lockfile reproduzierbar festlegen.

Abhängigkeiten zeigen nach innen:

HTTP/API → Application Services → Domain / Constraint Engine / Scoring
                         ↓
             Repositories und Source-Adapter
                         ↓
                    PostgreSQL
  • domain und scoring importieren weder FastAPI noch SQLAlchemy oder konkrete Quellen.
  • Die Engine ist eine reine Berechnung über explizite Eingaben. Kein Netzwerkzugriff, kein DB-Zugriff und kein implizites Lesen der Systemzeit.
  • Services laden Daten, erzeugen einen konsistenten Snapshot, rufen die Engine auf und speichern das Ergebnis atomar.
  • API-Schemas und ORM-Modelle sind getrennt von Domain-Modellen. Kleine Dataclasses oder Pydantic-Modelle sind für die Domain zulässig.
  • Regeln liegen als validierte Daten vor; keine Ausführung von Python, SQL, eval oder Templates aus Profilen.
  • n8n bleibt später ein externer Aufrufer. Die gesamte Buying Engine funktioniert ohne n8n.

3. Fachliche Entitäten und Identität

Product

Produktfamilie bzw. Modell, beispielsweise „Example Mobile 13“. Felder: UUID id, category, brand, name, optional model, created_at. Keine RAM-, SSD- oder Preisfelder. Kategorie ist ein normalisierter String, kein Notebook-spezifisches Enum.

ProductVariant

Eine eindeutig beschriebene technische Konfiguration eines Products. Felder: UUID id, product_id, name, optionale Hersteller-SKU manufacturer_sku, optionale Kennungen identifiers als JSON-Objekt, created_at.

RAM, Speicher, Farbe, Tastaturlayout und Ausstattung gehören zur Variante. Angebote unterschiedlicher Konfigurationen dürfen nicht zusammengeführt werden. In v0.1 verknüpft der Aufrufer Varianten explizit; keine heuristische Identitätsauflösung. Hersteller-SKUs sind nicht global eindeutig. Identische Namen sind kein Dublettenbeweis.

Offer

Ein konkretes Händlerangebot für genau eine Variante. Felder: UUID id, variant_id, source_id, external_id, seller, optionale url, market (z. B. DE), currency (dreistelliger Großbuchstaben-Code), condition (new, refurbished, used), created_at.

Eindeutigkeit: (source_id, external_id). Unterschiedliche Verkäufer, Zustände, Märkte oder Lieferumfänge sind unterschiedliche Angebote. Technisch abweichende Bundles erfordern eine passende Variante. Diese Identitätsfelder bleiben nach Erstellung unverändert. Ein Offer enthält keinen unabhängig veränderbaren „aktuellen Preis“.

PriceObservation

Append-only-Beobachtung eines Offers zu einem Zeitpunkt. Felder: UUID id, offer_id, source_id, external_observation_id, observed_at, ingested_at, item_price, nullable shipping_price, currency, availability (in_stock, out_of_stock, unknown), raw_payload als JSON, confidence.

  • Geld als Decimal/PostgreSQL NUMERIC(12,2), nie binäre Floats. API-Geldwerte werden als Dezimalstrings übertragen.
  • item_price > 0; bekannte Versandkosten >= 0; null bedeutet unbekannt, nicht kostenlos.
  • Gesamtpreis = Bruttopreis inklusive bekannter obligatorischer Bestandteile + Versand. Nur direkt für jeden Käufer verfügbare Preise erfassen; keine Mitgliederrabatte, Finanzierung oder personalisierte Coupons modellieren.
  • Die Währung muss der Offer-Währung entsprechen. source_id muss der Offer-Quelle entsprechen.
  • Zeitzonen sind obligatorisch; Speicherung und Ausgabe in UTC. Zeitpunkte in der Zukunft relativ zur Ingestion werden abgewiesen.
  • Eindeutigkeit (offer_id, external_observation_id): identischer Wiederholungsimport liefert den vorhandenen Datensatz, widersprüchlicher Inhalt HTTP 409. ingested_at ist serverseitig und zählt beim Inhaltsvergleich nicht mit.
  • Beobachtungen werden nicht überschrieben. Korrekturen werden als neue Beobachtung mit neuer externer ID erfasst; ein administrativer Korrekturworkflow ist nicht Bestandteil von v0.1.

4. Generische Attribute und Normalisierung

Attribute werden für v0.1 ausschließlich an Varianten erfasst. Es gibt keine implizite Vererbung von Product-Attributen. Eine spätere Erweiterung muss eine eigene, explizite Vorrangregel definieren.

Attributdefinition

attribute_definitions enthält key, value_type (number, string, boolean, string_list), nullable canonical_unit, optionale minimum/maximum und description. Schlüssel sind stabile snake_case-Bezeichner. Neue Definitionen lassen sich per API ohne Migration anlegen. Bestehende Definitionen sind in v0.1 unveränderlich; geänderte Semantik erhält einen neuen Schlüssel.

Beispieldefinitionen:

key type canonical_unit Wertebereich
ram_gb number GB >= 0
storage_gb number GB >= 0
weight_kg number kg > 0, im Eingabevalidator
battery_score number null 0–100
display_score number null 0–100
cpu_score number null 0–100
ports string_list null beliebige Strings

*_score-Attribute sind ausdrücklich manuelle Einschätzungen auf einer dokumentierten 0–100-Skala; die Engine erfindet keine Benchmarks oder Messwerte.

Attributwert

Jede Zeile in variant_attributes ist eine unveränderliche Aussage einer Quelle:

{
  "attribute": "ram_gb",
  "raw_value": "24GB LPDDR5X RAM",
  "normalized_value": 24,
  "unit": "GB",
  "normalization_status": "normalized",
  "normalizer_version": "manual-v1",
  "confidence": 1.0,
  "source_id": "manual",
  "source_reference": "demo/notebook-a/ram",
  "observed_at": "2026-10-03T10:00:00Z"
}

Zusätzlich serverseitig: UUID id, variant_id, ingested_at. raw_value ist verpflichtend und bleibt inhaltlich unverändert erhalten; es darf ein JSON-Skalar oder -Objekt sein. Bei Speicherung als JSONB ist keine bytegenaue Erhaltung der ursprünglichen JSON-Serialisierung garantiert. Falls eine spätere Quelle den exakten Response-Body benötigt, muss dieser separat als Rohtext gespeichert werden; das ist nicht Bestandteil von v0.1. normalized_value ist ein typisierter JSON-Wert. Bei normalization_status=unknown ist er null; ein optionales normalization_error erklärt den fehlenden Wert. Eine erfolglose Normalisierung ist kein Zahlenwert 0.

Manuelle Erfassung liefert Raw- und Normalized-Wert explizit. ManualNormalizer validiert Typ, Bereich und kanonische Einheit, analysiert aber keinen Freitext. Falsche Typen oder Einheiten werden mit 422 abgewiesen. Keine automatische Umwandlung von "32" zu 32 oder true zu 1. confidence liegt zwischen 0 und 1 und bezeichnet die Zuverlässigkeit der Aussage, nicht den Produktnutzen. Eine absichtlich unbekannte Aussage ist zulässig.

Bei mehreren Aussagen zu demselben Schlüssel verwendet der Snapshot die jüngste nach (observed_at, ingested_at, id) absteigend, jeweils nur bis zum Auswertungszeitpunkt bzw. Snapshot-Cutoff. Anschließend wird deren Confidence geprüft. Ein neuer unsicherer oder unbekannter Wert darf nicht stillschweigend durch einen älteren günstigen Wert ersetzt werden. Vorrang verschiedener Quellen ist damit zunächst bewusst einfach; spätere Auswahlstrategien benötigen eine neue Regelversion.

5. Product Profile und Versionierung

Ein Profil beschreibt Kategorie, Budget, harte Mindestanforderungen und gewichtete Präferenzen. Es besitzt eine stabile String-ID und eine positive Integer-Version. Jede (id, version) ist unveränderlich. Eine Änderung wird als neue Version gespeichert. Evaluationen müssen eine konkrete Version angeben; kein implizites „latest“.

Vollständiges Beispiel für profiles/mobile-development-notebook.json:

{
  "id": "mobile-development-notebook",
  "version": 1,
  "name": "Mobiler Entwicklungsrechner",
  "category": "notebook",
  "currency": "EUR",
  "market": "DE",
  "allowed_conditions": ["new"],
  "budget": {"target": "1500.00", "maximum": "2000.00"},
  "scoring_version": "generic-v1",
  "constraints": [
    {"id": "min_ram", "attribute": "ram_gb", "operator": ">=", "value": 16, "mandatory": true},
    {"id": "min_storage", "attribute": "storage_gb", "operator": ">=", "value": 512, "mandatory": true},
    {"id": "max_weight", "attribute": "weight_kg", "operator": "<=", "value": 1.6, "mandatory": true}
  ],
  "preferences": [
    {"id": "ram", "attribute": "ram_gb", "strategy": "numeric_linear", "direction": "higher", "acceptable": 16, "ideal": 32, "weight": 0.30},
    {"id": "weight", "attribute": "weight_kg", "strategy": "numeric_linear", "direction": "lower", "acceptable": 1.6, "ideal": 1.0, "weight": 0.25},
    {"id": "battery", "attribute": "battery_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.20},
    {"id": "display", "attribute": "display_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.15},
    {"id": "cpu", "attribute": "cpu_score", "strategy": "numeric_linear", "direction": "higher", "acceptable": 60, "ideal": 100, "weight": 0.10}
  ]
}

Jede Präferenz besitzt eine explizite strategy. v0.1 unterstützt ausschließlich numeric_linear. Das Profilformat ist jedoch so zu modellieren, dass spätere Scoring-Versionen zusätzliche klar benannte Strategien wie boolean_preferred, categorical_match, set_contains oder categorical_rank einführen können, ohne die Grundstruktur eines Profils zu ändern. Für v0.1 wird dafür weder ein Plugin-System noch dynamische Code-Ausführung gebaut; unbekannte Strategien werden bei der Profilvalidierung abgewiesen.

Validierung: 0 < target < maximum, mindestens eine Präferenz, positive Gewichte, innerhalb der jeweiligen Liste eindeutige IDs, bekannte Attributschlüssel und eine vorhandene Scoring-Version. Die Summe der Gewichte muss nicht 1 sein, weil die Engine normalisiert. Für numeric_linear gilt: higher verlangt ideal > acceptable, lower verlangt ideal < acceptable, und das referenzierte Attribut muss numerisch sein. Kategorie, Währung, Markt und erlaubte Zustände sind verpflichtend.

Das Profil ist ein Beispiel aus der Architekturentscheidung, keine abschließend optimierte persönliche Kaufberatung. Die Auswahl eines Notebooks für mobile Entwicklung wird nicht mit Anforderungen an einen lokalen LLM-Server vermischt.

6. Constraint Engine und Eignungsstatus

Unterstütze ausschließlich folgende Operatoren; verwerfe ungültige Kombinationen bereits beim Anlegen des Profils:

Operator Attributtyp Vergleichswert / Semantik
>=, <=, >, < number Zahl in kanonischer Einheit
==, != number, string, boolean gleicher skalarer Typ, exakter Vergleich
in number, string, boolean nichtleere Liste gleichartiger skalarer Werte
contains string_list einzelner String, exakte Mitgliedschaft
exists alle kein value; verwertbarer normalisierter Wert vorhanden

Keine impliziten Typkonvertierungen, String-Teilstrings oder case-insensitive Vergleiche. Leere String-Listen sind vorhandene Werte. Fehlend, null, nicht normalisiert oder Confidence unter der Regelgrenze ergibt UNKNOWN, auch bei exists und !=.

Jedes Ergebnis enthält Constraint-ID, Attribut, Operator, Sollwert, Istwert, Status PASS|FAIL|UNKNOWN, Mandatory-Flag, gewählte Attributzeilen-ID und Reason-Code.

Product Eligibility

product_eligibility beschreibt ausschließlich, ob die konkrete Produktvariante fachlich zum Profil passt. Dazu gehören:

  • Kategorie des Produkts/der Variante,
  • alle mandatory Produkt-Constraints,
  • technische bzw. produktbezogene Daten, die für diese Constraints benötigt werden.

Ableitung:

  • mindestens ein bekannter produktbezogener harter FAIL → product_eligibility=INELIGIBLE;
  • sonst mindestens ein mandatory produktbezogener UNKNOWN → product_eligibility=UNKNOWN;
  • sonst → product_eligibility=ELIGIBLE.

Optionale Constraints ändern product_eligibility nicht. Optionales FAIL oder UNKNOWN reduziert ausschließlich den ProductScore um die im Scoring-Modell konfigurierte Punktzahl und bleibt in den Ergebnissen sichtbar.

Der ProductScore darf unabhängig von der aktuellen Angebotslage berechnet und als Diagnostik gespeichert werden, sofern das Profil und die vorhandenen Präferenzdaten dies zulassen. Ein technisch sehr gutes Produkt verliert seinen ProductScore nicht nur deshalb, weil das betrachtete Offer aktuell zu teuer oder nicht lieferbar ist.

Offer Eligibility

offer_eligibility beschreibt ausschließlich die Eignung des konkret ausgewerteten Offers. Systemseitige Offer-Gates sind:

  • Markt entspricht dem Profil,
  • Zustand ist in allowed_conditions,
  • Währung entspricht der Profilwährung,
  • aktuelle Preisbeobachtung ist vorhanden und frisch,
  • Preis-Confidence erfüllt die Mindestschwelle,
  • obligatorische Versandkosten sind bekannt,
  • Gesamtpreis <= Budgetmaximum,
  • aktuelle Verfügbarkeit ist in_stock.

Ein bekannter Verstoß ergibt offer_eligibility=INELIGIBLE. Fehlende/zu alte Preisbeobachtung, geringe Preis-Confidence, unbekannte Versandkosten oder unbekannte Verfügbarkeit ergeben offer_eligibility=UNKNOWN. out_of_stock ergibt INELIGIBLE. Ein Preis oberhalb des Budgets ist ein Angebotsproblem und macht nicht das Produkt selbst ungeeignet.

Overall Eligibility

overall_eligibility wird ausschließlich aus Product und Offer Eligibility abgeleitet:

  • wenn mindestens einer der beiden Status INELIGIBLE ist → overall_eligibility=INELIGIBLE;
  • sonst wenn mindestens einer UNKNOWN ist → overall_eligibility=UNKNOWN;
  • sonst → overall_eligibility=ELIGIBLE.

Alle Gates werden ausgewertet, damit die Antwort sämtliche Gründe enthält. Ein bekannter harter Fehler hat innerhalb der jeweiligen Ebene Vorrang vor unbekannten Daten. Das API-Feld eligible ist eine Convenience-Abbildung von overall_eligibility: ELIGIBLE → true, INELIGIBLE → false, UNKNOWN → null.

7. Scoring-Modell generic-v1

Alle Scores liegen zwischen 0 und 100. Rechne intern mit Decimal, runde nur endgültige Scores und angezeigte Komponenten auf zwei Dezimalstellen, ROUND_HALF_UP. Klassifikation anhand des ungerundeten DealScore. Der Snapshot enthält auch ungerundete Dezimalstrings für eine eindeutige Grenzfallprüfung.

Die unveränderliche Modelldefinition enthält mindestens:

{
  "version": "generic-v1",
  "algorithm_id": "linear-budget-history-v1",
  "supported_preference_strategies": ["numeric_linear"],
  "minimum_confidence": 0.8,
  "maximum_price_age_hours": 48,
  "optional_constraint_penalty_points": 5,
  "missing_preference_score": 0,
  "product_weight": 0.6,
  "price_weight": 0.4,
  "budget_weight": 0.4,
  "history_weight": 0.6,
  "history_window_days": 90,
  "minimum_history_days": 3,
  "history_discount_for_max_score": 0.2,
  "classification_upper_bounds": [
    {"upper": 60, "inclusive": false, "label": "IGNORE"},
    {"upper": 75, "inclusive": false, "label": "WATCH"},
    {"upper": 85, "inclusive": false, "label": "GOOD"},
    {"upper": 92, "inclusive": true, "label": "VERY_GOOD"},
    {"upper": 100, "inclusive": true, "label": "EXCEPTIONAL"}
  ]
}

clamp(x)=min(100,max(0,x)). Modellvalidierung prüft positive Zeitfenster, gültige Confidence, nichtnegative Gewichte mit jeweiliger Summe 1, aufsteigende lückenlose Klassifikationsgrenzen, unterstützte Algorithmus-ID und eine nichtleere Liste unterstützter Preference-Strategien. Die Definition wird als kanonisches JSON inklusive SHA-256 gespeichert. Eine Version darf niemals mit geändertem Inhalt wiederverwendet werden. Neue Algorithmen oder Preference-Strategien werden als Code ergänzt und unter einer neuen Scoring-Version bzw. Algorithmus-ID registriert; JSON darf keinen ausführbaren Code enthalten.

ProductScore

v0.1 implementiert ausschließlich die Preference-Strategie numeric_linear. Für eine solche numerische Präferenz mit Istwert x, akzeptablem Wert a und Idealwert i:

higher: component = clamp(50 + 50 × (x − a) / (i − a))
lower:  component = clamp(50 + 50 × (a − x) / (a − i))
base_product_score = Σ(weight × component) / Σ(weight)
product_score = clamp(base_product_score − 5 × Anzahl optionaler FAIL/UNKNOWN)

Damit erhält ein akzeptabler Wert 50, ein idealer oder besserer Wert maximal 100. Ein fehlender oder unsicherer Präferenzwert erhält 0; sein Gewicht bleibt im Nenner. Fehlende Daten dürfen den Score nicht verbessern. product_coverage ist die Summe der Gewichte verwertbarer Präferenzen geteilt durch die Summe aller Gewichte. Confidence wird nicht nochmals als Score-Multiplikator verwendet.

Der ProductScore beschreibt Produkt-Fit und wird unabhängig von offer_eligibility berechnet. Auch bei product_eligibility=INELIGIBLE oder UNKNOWN darf er diagnostisch ausgegeben werden; Mandatory Gates bleiben jedoch autoritativ und können niemals durch einen hohen ProductScore überstimmt werden.

Die Strategy-Auswahl erfolgt explizit über das Preference-Feld strategy; es gibt keine dynamische Plugin-Suche. Unbekannte Strategien sind für generic-v1 ein Validierungsfehler.

Auswahl des aktuellen Preises und der Referenzhistorie

Der Evaluation-Request bezeichnet genau ein Offer. Kein implizites billigstes Angebot und kein Vermischen mehrerer Händler oder Varianten.

v0.1 verwendet als reference_history ausschließlich Preisbeobachtungen dieses konkreten Offers. Diese Bindung ist eine Entscheidung der Scoring-Version, nicht des generischen Preis-Scoring-Vertrags. Die reine Preis-Scoring-Funktion ist daher konzeptionell als Berechnung über current_price, reference_history und budget zu implementieren; sie darf fachlich nicht verlangen, dass die Referenzserie dauerhaft ein Offer-Objekt ist. Spätere Scoring-Versionen dürfen Variant-Level- oder Market-Level-Referenzserien bereitstellen, ohne Product, ProductVariant, Offer oder PriceObservation neu zu modellieren.

Für v0.1 gilt für die offer-bezogene Referenzserie:

  1. Betrachte ausschließlich Beobachtungen dieses Offers mit observed_at <= as_of und ingested_at <= snapshot_cutoff.
  2. Aktuell ist die jüngste Zeile nach (observed_at, ingested_at, id) absteigend. Auch eine nicht verfügbare oder unsichere jüngste Zeile darf nicht durch eine ältere günstigere ersetzt werden.
  3. Frische: as_of − observed_at <= 48 Stunden, einschließlich Grenze.
  4. Historie endet strikt vor dem UTC-Kalendertag der aktuellen Beobachtung. Dadurch wird der aktuelle Angebotspreis nicht selbst zur historischen Referenz.
  5. Für jedes Fenster N ∈ {30,90,365} gilt [current_day − N Tage, current_day). Wähle zunächst pro UTC-Tag die jüngste Zeile, danach filtere Tage mit in_stock, bekannter Gesamtsumme und Confidence >= 0.8. So verdrängt eine spätere unbekannte Beobachtung eine frühere bekannte desselben Tages.
  6. Berechne median_30d, median_90d, lowest_365d aus diesen Tageswerten. Bei gerader Anzahl ist der Median das arithmetische Mittel der beiden mittleren Werte. Gib die Zahl der vorhandenen Tage pro Fenster mit aus; keine Interpolation fehlender Tage.

Historienaggregate werden berechnet und im Evaluation-Snapshot gespeichert, nicht als vom Client beliebig vorgegebene Wahrheit übernommen. Die v0.1-Historie ist angebotsbezogen und keine Aussage über den Gesamtmarkt. lowest_365d darf als Minimum weniger beobachteter Tage angezeigt werden, muss dann ausdrücklich als Minimum der vorhandenen Daten bezeichnet sein. Der Snapshot speichert zusätzlich price_reference_scope="offer" und die IDs der verwendeten Beobachtungen, damit spätere Scoring-Versionen andere Scopes eindeutig unterscheiden können.

PriceScore

Seien p aktueller Gesamtpreis, t Budgetziel, m Budgetmaximum, r 90-Tage-Median der von der Scoring-Version bereitgestellten Referenzhistorie:

budget_score = 100                              falls p <= t
budget_score = clamp(100 × (m − p) / (m − t))   sonst
discount = (r − p) / r
history_score = clamp(50 + 50 × discount / 0.20)
price_score = 0.40 × budget_score + 0.60 × history_score

Historien-Score: beim Median 50, bei 20 % günstiger 100, bei 20 % teurer 0. Nur bei mindestens drei verfügbaren Tagen im 90-Tage-Fenster ist history_score vorhanden. Andernfalls gilt price_score=budget_score, price_basis=BUDGET_ONLY, history_score=null und ein expliziter Hinweis INSUFFICIENT_HISTORY. Bei ausreichender Historie price_basis=BUDGET_AND_HISTORY. Kein Bonus für ein vermeintliches Jahrestief; 30-Tage-Median und Minimum dienen zunächst nur der Erklärung.

Ohne frischen, hinreichend sicheren und vollständig bekannten Gesamtpreis in Profilwährung ist der PriceScore null. Ein bekannter Preis oberhalb des Budgets kann diagnostisch bewertet werden, während offer_eligibility wegen des Budget-Gates INELIGIBLE bleibt. Bei falscher Währung wird ohne Wechselkursumrechnung kein PriceScore berechnet. Ausverkaufte Angebote dürfen ihren bekannten Preis diagnostisch behalten; offer_eligibility bleibt dennoch INELIGIBLE.

DealScore und Klassifikation

Nur bei overall_eligibility=ELIGIBLE und vorhandenem PriceScore:

deal_score = 0.60 × product_score + 0.40 × price_score
Bereich des ungerundeten DealScore Klassifikation
< 60 IGNORE
>= 60 und < 75 WATCH
>= 75 und < 85 GOOD
>= 85 und <= 92 VERY_GOOD
> 92 EXCEPTIONAL

Bei overall_eligibility=INELIGIBLE: DealScore null, Klassifikation INELIGIBLE. Bei overall_eligibility=UNKNOWN: DealScore null, Klassifikation INSUFFICIENT_DATA. ProductScore und vorhandener PriceScore dürfen als Diagnostik ausgegeben werden, ersetzen aber niemals die Gate-Ergebnisse. Die getrennten Product-/Offer-Status müssen in Snapshot und API sichtbar bleiben, damit ein gutes Produkt mit momentan schlechtem Angebot weiterhin beobachtbar bleibt.

Nachvollziehbarkeit und Wiederholbarkeit

Jede Evaluation speichert Profil und Version, Modellversion und Hash, Engine-Build-ID, as_of, snapshot_cutoff, product_eligibility, offer_eligibility, overall_eligibility, vollständige tatsächlich verwendete Eingaben und IDs sowie alle Komponenten, Gates, Aggregate, Coverage, Price-Reference-Scope, Reason-Codes und verständliche Texte. Persistierte Evaluationen sind unveränderlich. Ein erneuter Request erzeugt eine neue Evaluation; ein GET berechnet nichts neu.

Historische Abfragen mit as_of rekonstruieren nur den Datenstand zum aktuellen Snapshot-Cutoff, einschließlich später eingegangener Rückdatierungen. Sie behaupten nicht, den damaligen Wissensstand wiederherzustellen. Exakte Wiederholung einer alten Bewertung erfolgt intern aus deren gespeichertem Snapshot und zugehöriger Algorithmusversion. Alte Algorithmen bleiben verfügbar; kein Replay-HTTP-Endpunkt für v0.1 erforderlich.

8. Source-Adapter

Implementiere einen kleinen Protocol und ManualSource. Quell-DTOs enthalten Rohdaten, Quellenkennung, externe IDs, Zeitpunkte und Confidence; sie enthalten keine ORM-Objekte und keine Scoring-Logik.

class ProductSource(Protocol):
    source_key: str
    def get_product(self, external_id: str) -> SourceProduct: ...
    def get_offers(self, external_product_id: str) -> list[SourceOffer]: ...
    def get_price_history(self, external_offer_id: str) -> list[SourcePriceObservation]: ...

ManualSource liest einen validierten lokalen Demo-Datensatz, keine externe Website. Ein Import-Service nimmt den Adapter entgegen und legt über dieselben Application Services wie die API Produkte, Varianten, Attribute, Angebote und Beobachtungen an. Explizite Variantenzuordnung aus der Fixture; keine Matching-Engine. Wiederholter Demo-Import erzeugt keine Duplikate: stabile UUIDs für Produkte/Varianten/Attribute, stabile externe Offer-/Beobachtungs-IDs und identischer Inhalt; Widersprüche melden statt überschreiben.

Adapter normalisieren keine Preise oder technischen Eigenschaften im Scoring-Modul. Der Pfad lautet Quelle → validierte DTOs → Normalisierung → Persistenz → Evaluation. Für spätere Quellen genügen zunächst Adapter und Import-Mapping. Pagination, Rate Limits und Retry-Strategien erst zusammen mit der ersten realen Quelle einführen.

9. HTTP-API

Alle fachlichen Endpunkte unter /api/v1. UUIDs für Entitäten, String-IDs für Profile, Attribute und Scoring-Modelle. Pydantic-Schemas mit extra=forbid; Pflichtfelder und Enums im OpenAPI-Schema dokumentieren. JSON-Ausgabe nutzt snake_case.

Methode / Pfad Vertrag
GET /health/live Prozess lebt, kein DB-Zugriff
GET /health/ready DB erreichbar und Migration auf Head; sonst 503
POST /api/v1/attribute-definitions unveränderliche Definition anlegen
GET /api/v1/attribute-definitions Definitionen listen
GET /api/v1/sources registrierte Quellen, einschließlich manual
GET /api/v1/scoring-models/{version} vollständige Regeldefinition + Hash
POST /api/v1/profiles vollständiges Profil inklusive Version anlegen
GET /api/v1/profiles konkrete Profilversionen listen
GET /api/v1/profiles/{id}?version=1 konkrete Version; Query verpflichtend
POST /api/v1/products Product anlegen
GET /api/v1/products/{id} Product lesen
POST /api/v1/products/{id}/variants Variante anlegen
GET /api/v1/variants/{id} Variante mit Attributaussagen lesen
POST /api/v1/variants/{id}/attributes einzelne unveränderliche Attributaussage hinzufügen
POST /api/v1/variants/{id}/offers Offer anlegen
GET /api/v1/offers/{id} Offer lesen
POST /api/v1/offers/{id}/prices einzelne Beobachtung hinzufügen bzw. identisch wiederholen
GET /api/v1/offers/{id}/prices gespeicherte Beobachtungen listen
POST /api/v1/evaluate/{variant_id} auswerten und Ergebnis atomar speichern
GET /api/v1/evaluations/{id} gespeichertes Ergebnis inklusive Snapshot lesen

Listen verwenden limit (Default 50, maximal 200) und offset (>=0); Antwort {items, limit, offset, total}. Stabile Sortierung nach Primärschlüssel, bei Preisen (observed_at, ingested_at, id) aufsteigend. Keine Update-/Delete-Endpunkte in v0.1.

Evaluation-Request:

{
  "profile_id": "mobile-development-notebook",
  "profile_version": 1,
  "offer_id": "33333333-3333-4333-8333-333333333333",
  "as_of": "2026-10-03T12:00:00Z"
}

as_of ist optional; der Service setzt es einmal auf Serverzeit UTC. Zukunftswerte sind ungültig. Offer muss zur angegebenen Variante gehören (422). Das Profil legt die Scoring-Version fest; kein stiller Override. Fachlich unpassende Kategorie wird im Product-Gate gespeichert; Markt, Zustand, Währung, Preis und Verfügbarkeit werden im Offer-Gate gespeichert. Keiner dieser fachlichen Fälle ist ein HTTP-Fehler.

Evaluation-Response enthält mindestens:

{
  "id": "44444444-4444-4444-8444-444444444444",
  "profile_id": "mobile-development-notebook",
  "profile_version": 1,
  "scoring_version": "generic-v1",
  "product_eligibility": "ELIGIBLE",
  "offer_eligibility": "ELIGIBLE",
  "overall_eligibility": "ELIGIBLE",
  "eligible": true,
  "product_score": 90.21,
  "price_score": 98.24,
  "deal_score": 93.42,
  "classification": "EXCEPTIONAL",
  "price_basis": "BUDGET_AND_HISTORY",
  "product_coverage": 1.0,
  "failed_constraints": [],
  "unknown_constraints": [],
  "reasons": [
    {"code": "PRICE_BELOW_MEDIAN", "message": "Gesamtpreis 18,83 % unter dem Median der beobachteten 90-Tage-Historie."}
  ]
}

Zusätzlich verpflichtend: variant_id, offer_id, as_of, snapshot_cutoff, scoring_hash, engine_build, product_eligibility, offer_eligibility, overall_eligibility, components, sämtliche constraint_results und Offer-Gates, price_statistics, price_reference_scope, input_snapshot und warnings. Die Kurzantwort oben illustriert nur die Kernfelder. failed_constraints enthält alle FAIL-Ergebnisse; unknown_constraints alle UNKNOWN-Ergebnisse. Reason-Codes sind stabil, Texte dienen Menschen und dürfen nicht als Programmierschnittstelle ausgewertet werden.

POST liefert 201, identisch wiederholte Preisbeobachtung 200; GET liefert 200. Fehlende IDs: 404. Verletzte Eindeutigkeit/abweichender Wiederholungsinhalt: 409. Ungültige Payload/Referenzkombination: 422. Einheitliches Fehlerformat {error: {code, message, details}}, auch für Request-Validierung. Keine SQL-Details an Clients. Eine fachlich negative Evaluation ist eine erfolgreiche 201-Antwort.

10. Datenbank und Transaktionen

Alle Zeitstempel TIMESTAMPTZ; JSON-Daten JSONB; UUID-Primärschlüssel für Entitäten; Decimal-Geldfelder. created_at/ingested_at werden serverseitig gesetzt. Fremdschlüssel sind obligatorisch. Kein Cascade-Delete historischer Bewertungen oder Quelldaten.

Tabelle Wesentliche Spalten und Constraints
attribute_definitions key PK, Typ, Einheit, Bereich, Beschreibung
sources id String-PK, name, adapter_type; Seed manual
scoring_models version PK, algorithm_id, definition JSONB, definition_hash, created_at
product_profiles zusammengesetzter PK (id, version), definition JSONB, category, FK scoring_version, definition_hash, created_at
products id, Kategorie, Marke, Name, Modell, created_at
product_variants id, FK Product, Name, Hersteller-SKU, Kennungen JSONB, created_at
variant_attributes id, FK Variante/Definition/Quelle, Raw/Normalized JSONB, Einheit, Status, Normalizer-Version, Confidence, Source-Referenz, Beobachtungs-/Ingestion-Zeit, Fehler
offers id, FK Variante/Quelle, externe ID, Händler, URL, Markt, Währung, Zustand; UNIQUE Quelle/externe ID
price_observations id, FK Offer/Quelle, externe Beobachtungs-ID, Preise, Währung, Verfügbarkeit, Confidence, Raw JSONB, beide Zeiten; UNIQUE Offer/externe ID
evaluations id, FK Variante/Offer/Profilversion/Scoring-Version, Zeitpunkte, Hash, Build-ID, product_eligibility, offer_eligibility, overall_eligibility, drei nullable Scores, Klassifikation, price_reference_scope, vollständige Eingabe-/Ergebnis-Snapshots JSONB
evaluation_components id, FK Evaluation, kind, component_key, nullable Rohscore/Gewicht/Beitrag, details JSONB; UNIQUE Evaluation/kind/key

evaluation_components enthält Präferenzen inklusive Strategy, optionale Abzüge, Budget-/History-Komponenten, Product-Gates und Offer-Gates. JSON-Snapshots sind die vollständige Replay-Grundlage; relationale Komponenten dienen Abfragen und dürfen bei Erstellung nicht davon abweichen.

DB-Checks mindestens für nichtnegative Preise, positiven Artikelpreis, Confidence 0–1, Scores 0–100, Profilversion >0 und gültige feste Enums. Typ-/Einheiten-/Profilvalidierung bleibt zusätzlich in der Application-Schicht. Redundante Indexspalten eines Profils müssen aus dessen validierter Definition erzeugt werden.

Indizes: Attribute (variant_id, attribute_key, observed_at DESC, ingested_at DESC, id DESC), Preise (offer_id, observed_at DESC, ingested_at DESC, id DESC), Offers variant_id, Evaluationen (profile_id, profile_version, created_at DESC) und variant_id.

Eine Evaluation liest und schreibt innerhalb einer PostgreSQL-Transaktion mit konsistentem Snapshot (REPEATABLE READ). Zu Beginn snapshot_cutoff festlegen; komplette Inputs, Ergebnis und Komponenten atomar committen. Bei Fehlern kein Teilergebnis. Insert-Konflikte über UNIQUE-Constraints behandeln, nicht allein per vorherigem SELECT.

Alembic ist die einzige produktive Schemaanlage; kein metadata.create_all() beim API-Start. Initialmigration enthält Tabellen, Constraints und Indizes. Bootstrap von Quellen und Regeldefinitionen separat, idempotent und inhaltlich geprüft. Ein Konflikt gleicher Version mit anderem Hash stoppt den Bootstrap sichtbar.

11. Repository und Lieferumfang

buying-agent/
├── README.md
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env.example
├── .gitignore
├── pyproject.toml
├── uv.lock
├── alembic.ini
├── migrations/
│   ├── env.py
│   └── versions/
├── app/
│   ├── main.py
│   ├── settings.py
│   ├── api/
│   │   ├── schemas.py
│   │   ├── profiles.py
│   │   ├── products.py
│   │   ├── offers.py
│   │   ├── evaluations.py
│   │   └── health.py
│   ├── domain/
│   │   ├── models.py
│   │   ├── attributes.py
│   │   ├── constraints.py
│   │   └── enums.py
│   ├── scoring/
│   │   ├── product.py
│   │   ├── price.py
│   │   ├── deal.py
│   │   └── engine.py
│   ├── normalization/
│   │   ├── base.py
│   │   └── manual.py
│   ├── sources/
│   │   ├── base.py
│   │   └── manual.py
│   ├── persistence/
│   │   ├── db.py
│   │   ├── models.py
│   │   └── repositories.py
│   └── services/
│       ├── ingestion_service.py
│       └── evaluation_service.py
├── rules/generic-v1.json
├── profiles/mobile-development-notebook.json
├── fixtures/manual-demo.json
├── scripts/
│   ├── bootstrap.py
│   └── demo.py
└── tests/
    ├── unit/
    ├── integration/
    └── e2e/

__init__.py nach Bedarf ergänzen. Kleine Dateien dürfen sinnvoll zusammengelegt werden; Architekturgrenzen sind verbindlich. Kein generisches Repository-Framework und kein Plugin-Discovery-System erforderlich. Das Lockfile-Beispiel verwendet uv; uv als Build-/Entwicklungswerkzeug mit dokumentierter Version verwenden.

README enthält Setup, Architektur, Datenmodell, Formeln, Fehlersemantik, alle Start-/Testkommandos, vollständige Beispielrequests sowie Grenzen der synthetischen Preisbewertung. Diese Spezifikation ebenfalls im Repository unter docs/buying-agent-v0.1-spec-v1.1.md ablegen.

12. Docker und lokale Ausführung

Compose enthält nur zwei dauerhaft laufende Services: api und postgres. PostgreSQL 17 mit benanntem Datenvolume und Healthcheck. API basiert auf Python 3.13, läuft als Nicht-Root und bindet den veröffentlichten Port an 127.0.0.1:8000. Die Datenbank braucht keinen veröffentlichten Host-Port. Secrets und lokale DB-Zugänge über .env, nur Platzhalter in .env.example.

Für das lokale Einzelinstanz-Skeleton darf das API-Entrypoint nach erfolgreichem DB-Healthcheck sequenziell alembic upgrade head, Bootstrap und Uvicorn ausführen. Fehlgeschlagene Migrationen verhindern den API-Start. Diese Startmigration ist keine Strategie für spätere Mehrinstanz-Deployments.

Verbindlicher Startpfad:

cp .env.example .env
docker compose up --build -d
docker compose exec api python -m scripts.demo

Für PowerShell zusätzlich Copy-Item .env.example .env dokumentieren. scripts.demo legt Definitionen, Profil und synthetische Kandidaten über die API an, wiederholt sich ohne Duplikate und ruft Evaluationen mit explizitem as_of auf. Die Demo umfasst mindestens die Fälle A, B, C und D aus Abschnitt 13. Statische Fixtures nutzen den unten definierten historischen Zeitpunkt, damit sie nicht mit der Kalenderzeit altern. Der Adapterpfad wird separat im Integrationstest geprüft; die Demo muss den HTTP-Pfad abdecken.

Testabhängigkeiten in einer Docker-Build-Stage test bereitstellen. README dokumentiert einen ausführbaren Compose-Test-Override oder gleichwertigen Befehl mit dieser Stage und separater Testdatenbank. Tests dürfen niemals das normale Datenvolume leeren. Kein dauerhafter dritter Service nötig.

13. Deterministische Beispiele und Tests

Referenzfall A

as_of=2026-10-03T12:00:00Z, aktuelle Beobachtung um 10:00 UTC; alle technischen Attribute vorher beobachtet. Kategorie Notebook, Markt DE, Zustand neu, EUR, lieferbar, Confidence jeweils 1.0, Versand 0.00.

Attribut Wert Präferenzscore
ram_gb 32 100
storage_gb 1024 nur Mindestconstraint
weight_kg 1.2 83.333333…
battery_score 90 87.5
display_score 90 87.5
cpu_score 90 87.5

Gesamtpreise der Historie: 1599.00 am 2026-09-01, 1599.00 am 2026-09-15, 1599.00 am 2026-10-01; jeweils 10:00 UTC. Aktuell 1298.00. Alle Zeilen betreffen dasselbe Offer und sind lieferbar. Fixtures werden vor Evaluation vollständig importiert.

Erwartungen:

product_eligibility = ELIGIBLE
offer_eligibility = ELIGIBLE
overall_eligibility = ELIGIBLE; eligible = true
product_score = 90.21
median_30d = 1599.00 (2 Tage)
median_90d = 1599.00 (3 Tage)
lowest_365d = 1599.00 (3 Tage)
budget_score = 100
history_score = 97.06
price_score = 98.24
deal_score = 93.42
classification = EXCEPTIONAL
product_coverage = 1.0

Die Engine rechnet mit den ungerundeten Zwischenwerten. Die Response aus Abschnitt 9 bezieht sich auf diesen Fall. Das beobachtete Minimum von 1599.00 ist ausdrücklich kein belegtes Jahrestief des Marktes.

Weitere Demokandidaten

  • B: identisch zu A, aber 8 GB RAM → product_eligibility=INELIGIBLE, offer_eligibility=ELIGIBLE, overall_eligibility=INELIGIBLE, Mandatory RAM FAIL, ProductScore weiterhin diagnostisch vorhanden, kein DealScore.
  • C: identisch zu A, aber keine verwertbare Gewichtsangabe → product_eligibility=UNKNOWN, offer_eligibility=ELIGIBLE, overall_eligibility=UNKNOWN, Mandatory Gewicht UNKNOWN, ProductScore diagnostisch vorhanden, Klassifikation INSUFFICIENT_DATA, kein DealScore.
  • D: technisch identisch zu A, aber aktueller Gesamtpreis 2049.00 bei Budgetmaximum 2000.00 → product_eligibility=ELIGIBLE, offer_eligibility=INELIGIBLE, overall_eligibility=INELIGIBLE; ProductScore und diagnostischer PriceScore bleiben vorhanden, DealScore null. Dieser Fall beweist die Trennung von Produkt-Fit und Angebots-Fit.

Unit-Tests

  • Jeden Constraint-Operator mit PASS, FAIL, UNKNOWN; Gleichheitsgrenzen, strikte Typen, leere Listen und Confidence exakt 0.8 bzw. knapp darunter.
  • Preference-Strategie numeric_linear: akzeptabel 50, ideal 100, Clamping, lower/higher, nicht auf 1 summierte Gewichte, fehlende Werte und optionale Abzüge; unbekannte strategy wird für generic-v1 abgewiesen.
  • Budget bei Ziel/Maximum/darüber; History-Score bei −20 %, 0 % und +20 % Rabatt; BUDGET_ONLY bei 0–2 Historientagen.
  • Tagesauswahl vor Qualitätsfilter, Fenstergrenzen, Ausschluss des aktuellen Tages, Median gerader/ungerader Anzahl, identische Zeitstempel und stabile Tie-Breaks.
  • Keine Vermischung von Offers, Varianten, Währungen oder Zuständen; aktuelle Zeile unbekannt/ausverkauft ohne Rückfall auf ältere Zeilen; ProductScore bleibt von Offer-Gates unabhängig.
  • Frische exakt 48 Stunden/knapp darüber; unbekannter Versand; fehlender Preis; Kombination Mandatory FAIL und UNKNOWN.
  • Klassifikationsgrenzen 60, 75, 85, 92 und unmittelbar darüber; Rundung darf keine Klasse verschieben.
  • Referenzfall A einschließlich exakter gerundeter Resultate; andere Kategorie mit eigenen Attributen ohne Engine-Änderung; Fall D zur Trennung von Product/Offer Eligibility.
  • Preis-Scoring-Funktion wird mit explizitem current_price, reference_history und budget getestet; Domain-Test darf dafür kein Offer-ORM-Objekt benötigen. price_reference_scope=offer ist für v0.1 verpflichtend.
  • Replay aus gespeichertem Input trotz später hinzugefügter Attribute/Preise liefert identische fachliche Ergebnisse; generierte IDs und Speicherzeit sind kein Bestandteil des Vergleichs.

Integration und End-to-End

  • Gegen echtes PostgreSQL 17, nicht SQLite als Ersatz für JSONB, Numeric, Constraints oder Isolation.
  • Migration auf leerer Test-DB; erneutes Upgrade ohne Änderung; initialer Downgrade/Upgrade nur in wegwerfbarer Test-DB.
  • Bootstrap identisch wiederholbar, veränderte Regel mit gleicher Version wird abgewiesen.
  • CRUD-Anlage-/Lesepfade, validierte Profilreferenzen, 404/409/422, Pagination, exakte Decimal- und UTC-Roundtrips.
  • Doppelte Preisimporte auch bei konkurrierenden Requests: eine Beobachtung, kein stiller Content-Overwrite.
  • Gescheiterte Evaluation-Persistenz hinterlässt weder Evaluation noch Komponenten.
  • HTTP-End-to-End: Profil → Produkt → Variante → Attribute → Offer → Historie → Evaluation → GET; danach API-Neustart und erneutes GET desselben unveränderten Ergebnisses.
  • ManualSource-Import nutzt dieselben Services und erzeugt bei Wiederholung keine Duplikate.
  • Einfügen neuer Preisbeobachtungen ändert alte Evaluationen nicht; neue Evaluation verwendet neue Inputs.

14. Implementierungsreihenfolge

  1. Projekt, Settings, Docker, PostgreSQL und initiale Migration aufsetzen.
  2. Domain-Modelle, Definitions-/Profilvalidierung einschließlich strategy, getrennte Product-/Offer-Gates und reine Constraint-/Scoring-Funktionen samt Referenztests implementieren.
  3. Preis-Scoring als reine Berechnung über current_price, reference_history und budget implementieren; v0.1-Assembler liefert dafür ausschließlich offer-bezogene Historie und markiert price_reference_scope=offer.
  4. Repositories und transaktionalen Evaluation-Service integrieren.
  5. API, ManualSource und Bootstrap anschließen; ersten vollständigen Durchstich ausführen.
  6. Demo, Integrationstests, Fehlerfälle und nachvollziehbare Responses vervollständigen.
  7. README und sauberen Start auf leerem Volume prüfen; tatsächliche Testergebnisse dokumentieren.

Routineentscheidungen innerhalb dieser Spezifikation selbst treffen. Falls eine Vorgabe technisch widersprüchlich ist, den konkreten Konflikt dokumentieren und minimal auflösen; keine stillen fachlichen Formeländerungen oder Scope-Erweiterungen. Insbesondere keine Infrastruktur für spätere Preference-Strategien, Variant-/Market-Level-Preisreihen, n8n, LLMs oder Discovery vorwegnehmen.

15. Definition of Done

  • Ein frischer Checkout startet mit den dokumentierten Docker-Kommandos; Migration und Readiness funktionieren.
  • PostgreSQL persistiert Daten über API-/Container-Neustarts hinweg.
  • Alle vier Entitäten Product, ProductVariant, Offer, PriceObservation sind getrennt und korrekt verknüpft.
  • Attribute sind generisch und bewahren Raw inhaltlich unverändert, Normalized, Quelle, Confidence und Normalizer-Version.
  • Beispielprofil und generic-v1 liegen versioniert als Dateien vor und werden validiert gespeichert; jede Preference besitzt strategy, v0.1 akzeptiert ausschließlich numeric_linear.
  • Product-, Offer- und Overall-Eligibility werden getrennt berechnet, persistiert und über die API ausgegeben.
  • Ein technisch passendes Produkt behält ProductScore/Product Eligibility unabhängig davon, ob das konkrete Offer aktuell zu teuer, ausverkauft oder anderweitig ungeeignet ist.
  • Das Preis-Scoring ist fachlich über current_price, reference_history und budget entkoppelt; v0.1 verwendet ausschließlich price_reference_scope=offer und implementiert keine Variant-/Market-Level-Historie.
  • Demo liefert für A, B, C und D die festgelegten Ergebnisse über die API.
  • Scores, harte Gates, fehlende Daten, Historienbasis, Reference Scope und Begründungen sind sichtbar und reproduzierbar.
  • Evaluationen enthalten vollständige unveränderliche Snapshots; spätere Daten und Regelversionen verändern sie nicht.
  • Unit-, PostgreSQL-Integrations- und HTTP-End-to-End-Tests sind grün; Kommandos und Resultate sind dokumentiert.
  • Neue Kategorie und neue Attribute funktionieren ohne Schema- oder Engine-Änderung, solange sie von den in der gewählten Scoring-Version unterstützten Strategies/Operatoren abgedeckt werden.
  • Keine reale externe Quelle, kein n8n, keine LLM-Normalisierung, keine Web-UI, keine Auto-Discovery, keine Variant-/Market-Level-Preisaggregation und keine vorsorgliche Plugin-Infrastruktur wurden eingebaut.
  • README ermöglicht einem anderen Entwickler Setup, Demo, Tests und Verständnis der Grenzen ohne Chat-Kontext.

Abschlussbericht des Coding-Agenten: implementierter Umfang, relevante Entscheidungen, ausgeführte Prüfungen mit Ergebnissen und gegebenenfalls konkrete verbleibende Abweichungen. Nicht ausgeführte Tests ausdrücklich als solche benennen.

16. Future Extension Contract

v0.1 ist ein Walking Skeleton des Zielsystems. Die folgenden Fähigkeiten sind ausdrücklich keine v0.1-Deliverables, die Architektur darf ihre spätere Ergänzung jedoch nicht durch produktspezifische Schemata, fest verdrahtete Scoring-Annahmen oder n8n-interne Fachlogik unnötig blockieren:

  • mehrere externe Produkt- und Preisquellen,
  • source-spezifische Search-/Discovery-Adapter,
  • Variant-Level- und Market-Level-Preisreferenzserien,
  • weitere Produktkategorien ohne produktspezifische DB-Spalten,
  • weitere explizit versionierte Preference-Strategien wie boolean_preferred, categorical_match, set_contains oder categorical_rank,
  • LLM-gestützte Normalisierung mit Raw-Wert, normalisiertem Wert, Confidence und Normalizer-Version,
  • n8n für Scheduling, Orchestrierung und Benachrichtigungen,
  • automatische Kandidatensuche und Kandidatenaufnahme,
  • mehrere parallele Buying-Profile und Watchlists.

Dieser Vertrag verlangt keine vorsorgliche Infrastruktur für diese Funktionen. Insbesondere keine Plugin-Registry, keinen Event-Bus, keine Microservices, keine zusätzlichen Preisaggregationstabellen, keine LLM-Abstraktionsschicht und keine n8n-Workflows bauen, solange sie nicht für den aktuellen Slice erforderlich sind.

Die stabilen Erweiterungspunkte von v0.1 sind:

  1. Source-Adapter liefern validierte DTOs in den bestehenden Importpfad.
  2. Attribute und Constraints bleiben generisch und datengetrieben.
  3. Preference-Strategien werden explizit benannt und durch versionierte Scoring-Modelle unterstützt.
  4. PriceScore konsumiert eine von der Scoring-Version bereitgestellte Referenzserie statt dauerhaft an Offer-Identität gekoppelt zu sein.
  5. Product-, Offer- und Overall-Eligibility bleiben getrennt, damit Monitoring später gute Produkte unabhängig von momentan schlechten Angeboten verfolgen kann.
  6. n8n bleibt externer Orchestrator; die Buying Engine bleibt ohne n8n vollständig nutzbar und testbar.