From b98d2e599388feb2ac3410c2da85974144e43846 Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 23 Jul 2026 08:06:41 +0200 Subject: [PATCH] =?UTF-8?q?docs(13):=20phase=20context=20=E2=80=94=20scrap?= =?UTF-8?q?ing=20adapters,=20fuzzy=20cross-source=20dedup,=20denylist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../13-CONTEXT.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md diff --git a/.planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md b/.planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md new file mode 100644 index 0000000..35e561e --- /dev/null +++ b/.planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md @@ -0,0 +1,95 @@ +# 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 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). + +### Phase-11-Fundament (Anzeige der Multi-Source-Links) +- `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). + + + +## 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. + + + +## 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. +