Files
tessera-ctl/.planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md
T
2026-07-23 08:06:41 +02:00

8.0 KiB

Phase 13: Scraping Adapters & Cross-Source Deduplication - Context

Gathered: 2026-07-23 Status: Ready for planning Mode: mvp (vertical slices)

## Phase Boundary

Erweitert die Ausschreibungs-Abdeckung um zwei zusätzliche Quellen neben DÖE (Phase 10): AI-AG-NetServer-Portale (lhs-vpbw, tender24, vergabe.landbw) über EINEN config-getriebenen Adapter, und den cosinex-Vergabemarktplatz (DTVP) über einen separaten Adapter. Dieselbe Ausschreibung, die über mehrere Quellen auftaucht, wird zu einem Trefferlisten-Eintrag mit Links zu allen Quell-Portalen zusammengefasst (Fuzzy-Fingerprint-Dedup). Das System verweigert strukturell (im Code, nicht nur dokumentiert) das Registrieren der AGB-verbotenen Portale vergabe24 und aumass.

Requirements: INGEST-02, INGEST-03, INGEST-07, SCHEMA-03.

Nicht in dieser Phase: RSS/E-Mail-Quellen (Phase 14), Admin-Quellen-UI + ausgeschlossene-Portale-Anzeige UI-06 (Phase 14), i18n-Rollout (Phase 14). Keine neuen Benachrichtigungs-/Filter-Features (10-12 fertig).

## Implementation Decisions

Bestätigt durch User-Abfrage 2026-07-23:

Scraping-Tiefe

  • D-01: Robuster Kern zuerst, Scraping best-effort. Das config-getriebene Adapter-Framework, der Fuzzy-Dedup und die harte Denylist werden voll gebaut + getestet. Die konkreten Scraping-Selektoren/HTTP-Details werden so weit gefüllt, wie das jeweilige Portal es zulässt; blockende/instabile Portale werden dokumentiert (nicht erzwungen). Ziel: nachrüstbarer, getesteter Kern — kein fragiles All-or-Nothing-Scraping.
  • D-02: Machbarkeit der realen Portale (NetServer lhs-vpbw/tender24/vergabe.landbw, cosinex/DTVP) ist ein Research-Punkt (wie DÖE in Phase 10): Live prüfen, ob HTML-Scraping oder ein Export/Feed-Endpunkt existiert, Rate-Limits, robots/AGB. Ergebnis steuert, wie weit die Selektoren gefüllt werden.

Cross-Source-Dedup (SCHEMA-03)

  • D-03: Ein Eintrag + Liste ALLER Quell-Links. Eine über mehrere Quellen deduplizierte Ausschreibung ist EIN Tender, der alle Quell-Portale + deren Links/NoticeIds führt (neue Kind-Tabelle TenderSource, 1:n). Die Trefferliste zeigt einen Eintrag; die Detailansicht listet alle Quellen mit Links. NICHT „Primärquelle gewinnt, Rest verworfen".
  • D-04: Dedup-Schlüssel-Hierarchie (SCHEMA-03): OCID → Quelle:NoticeId → Fuzzy-Fingerprint aus Auftraggeber + Titel + CPV + Frist + Wert. Beim Ingest einer neuen Quelle: existiert OCID/NoticeId-Match → gleiche Ausschreibung; sonst Fingerprint gegen Bestand prüfen; Match → als zusätzliche TenderSource anhängen statt neuen Tender anzulegen.
  • D-05: Dedup greift erst ab der zweiten aktiven Quelle. Mit nur DÖE aktiv läuft keine Fuzzy-Dedup-Logik (Erfolgskriterium 3). Backfill der bestehenden ~2000+ DÖE-Tender: jeder bekommt eine TenderSource-Zeile aus seinen aktuellen sourcePortal/sourceNoticeId/sourceUrl.

Denylist (INGEST-07)

  • D-06: vergabe24 und aumass sind im Adapter-/Quellen-Registry als harte Denylist hinterlegt — der Versuch, sie als automatische Poll-/Scraping-Quelle zu registrieren, wird im Code abgelehnt (Exception/Fehler), nicht nur per Doku. Test beweist die Ablehnung. Grund: AGB-Verbot automatisierter Zugriffe; Oberschwellen-Daten kommen ohnehin über DÖE.

Claude's Discretion

  • Fingerprint-Algorithmus: Normalisierung (lowercase, Umlaute, Whitespace, CPV-Divisions-Präfix, Wert-Bucketing, Frist-Datum) + Hash oder Ähnlichkeits-Schwellwert; genaue Felder-Gewichtung. Muss NULL-tolerant sein (Frist/Wert oft null — Phase-11-Befund).
  • Adapter-Framework: Verallgemeinerung von tender-source-adapter.interface.ts (Phase 10) + Registry mit Denylist-Gate; wie TenderIngestionService.pollDueSources/TenderSourcePollConfig auf mehrere sourceType erweitert wird (poll-once-fan-out-many bleibt).
  • Scraping-Client pro Portal (native fetch, evtl. HTML-Parser), Pagination/Rate-Limit, Fehlertoleranz.
  • Datenmodell TenderSource: Felder, Unique-Constraints (z.B. @@unique([sourcePortal, sourceNoticeId])), Beziehung zu Tender, wie dedupKey/primäre sourcePortal-Felder migriert werden.

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Phasen-Vorgaben

  • .planning/ROADMAP.md § Phase 13 — Ziel + 4 Erfolgskriterien. Mode: mvp.
  • .planning/REQUIREMENTS.md — INGEST-02, INGEST-03, INGEST-07, SCHEMA-03 (+ INGEST-04/UI-06 sind Phase 14, NICHT hier).

Phase-10-Fundament (Adapter + Ingestion — verbindlich)

  • apps/api/src/tenders/tender-source-adapter.interface.ts — bestehende TenderSourceAdapter-Abstraktion; hier verallgemeinern + Registry.
  • apps/api/src/tenders/adapters/doe-opendata.adapter.ts (+ .spec) — Referenz-Adapter (Fetch → Normalize), Muster für NetServer/cosinex.
  • apps/api/src/tenders/tender-ingestion.service.ts (pollDueSources) + tender-scheduler.service.ts — poll-once-fan-out-many-Scheduler; auf mehrere Quellen erweitern (NICHT DKV-findFirst).
  • apps/api/src/tenders/tender-normalizer.service.ts — Normalisierung (buyer/cpv/region/bundesland/value); Fingerprint baut darauf auf.
  • apps/api/prisma/schema.prisma → model Tender (sourcePortal, sourceNoticeId, ocid, dedupKey @unique, contentHash, cpvCodes[], buyerName, deadlineAt, estimatedValue) und TenderSourcePollConfig (sourceType).
  • apps/web/.../modules/tender-radar/ (ResultsList, TenderDetail) — Detailansicht muss die TenderSource-Liste (mehrere Quell-Links) anzeigen; tenders.controller.ts Read-Endpunkte liefern sie mit.

Milestone-Research

  • .planning/research/ARCHITECTURE.md — TenderSourceAdapter-Abstraktion, Dedup-Strategie (OCID→Quelle:NoticeId→Fuzzy), global-vs-tenant-Split.
  • .planning/research/PITFALLS.md — Multi-Tenant-Scheduler, Scraping-Fallen, Dedup-Kollisionen.
  • .planning/research/ausschreibungs-portale-feasibility.md — bekannte Portal-Infos (DÖE + Ausblick NetServer/cosinex). </canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • tender-source-adapter.interface.ts + doe-opendata.adapter.ts — Adapter-Vertrag + Referenz.
  • tender-ingestion.service.ts / tender-scheduler.service.ts — Ingestion-Pipeline + Scheduler (poll-once-fan-out-many).
  • tender-normalizer.service.ts — Feld-Normalisierung; Grundlage fürs Fingerprinting.
  • tender-query.builder.ts / Phase-11-Results-UI — Trefferliste/Detail; erweitern um Multi-Source-Anzeige.
  • Native fetch als HTTP-Client (kein axios); Prisma-Migrationen handgeschrieben, lokal via docker compose exec -T db psql anwenden (kein Docker-Deploy Testserver).

Established Patterns

  • dedupKey @unique ist aktuell der SCHEMA-02-Upsert-Target (ocid | sourcePortal:sourceNoticeId). Für SCHEMA-03 kommt die Quell-Ebene in TenderSource; der kanonische Tender bleibt der eine Eintrag.
  • Hardcodiertes Deutsch (i18n = Phase 14). Per-user/tenant-Scoping nur wo nötig — Tender bleibt plattform-global (D-03 Phase 10).

Integration Points

  • Neue Tabelle TenderSource (1:n zu Tender) + Backfill der Bestands-Tender.
  • Adapter-Registry mit Denylist-Gate (vergabe24/aumass).
  • NetServer-Adapter (config-getrieben, mehrere Portale) + cosinex/DTVP-Adapter.
  • Fingerprint-Dedup im Ingestion-Upsert-Pfad (nur ab 2. aktiver Quelle).
  • Detailansicht/Results zeigen alle Quell-Links. </code_context>
## Specific Ideas
  • Erfolgskriterium 3 + 4 sind die harten Prüfsteine: (3) ein Tender aus DÖE+Scraper erscheint EINMAL mit allen Quell-Links; (4) vergabe24/aumass-Registrierung wird vom Code abgelehnt (Test).
  • „Robuster Kern, Scraping nachrüstbar" (D-01): Framework + Dedup + Denylist müssen auch dann getestet grün sein, wenn ein Portal live gerade nicht scrapebar ist — die Adapter-Grenze kapselt das.
  • Fingerprint muss NULL-tolerant sein (Frist/Wert oft leer — 91,6% ohne Wert, viele ohne Frist; Phase-11-Live-Befund), sonst kollabiert Dedup oder matcht falsch.
  • Konsistenz mit Phase 10: poll-once-fan-out-many, kein findFirst; Tender global.