--- phase: 13-scraping-adapters-cross-source-dedup kind: befund-behebung date: 2026-09-07 windows_ref: 11 quick_task: 260907-efh result: behoben und am laufenden System gegengeprueft --- # Phase 13 — die Dublettenerkennung greift wieder (WINDOWS #11) Der Verifikationsbericht vom 2026-07-23 schloss Phase 13 mit `gaps_found` ab: die dritte Stufe der Dublettenerkennung, der Fingerabdruck, legte die Mehrzahl echter Duplikate nicht zusammen. Der Befund stand seither nur in jenem Bericht und war praktisch unsichtbar, bis er am 2026-09-07 als WINDOWS.md #11 erfasst und am selben Tag behoben wurde. ## Wie schlimm es wirklich war Vor dem Fix, gemessen an der lokalen Datenbank mit 16.255 Ausschreibungen: | Messung | Wert | |---|---| | Zeilenpaare, die sich einen gespeicherten Fingerabdruck teilen | **0** | Nicht „zu wenige" — **null**. Die Stufe legte nichts zusammen, bei keiner einzigen Zeile. Sie war nicht ungenau, sondern wirkungslos. ## Warum Der Fingerabdruck hashte fünf Angaben: Vergabestelle, Titel, CPV-Division, Frist und geschätzten Wert. Zwei davon liefert nur eine Quellenart. `normalizeBag()` — der Weg für ai-netserver, cosinex, RSS und E-Mail-Alarm — setzt `cpvDivisions` fest auf leer und `estimatedValue` fest auf null; der DÖE-Weg füllt beide. | Quelle | Zeilen | mit CPV | mit Wert | mit Frist | mit Vergabestelle | |---|---|---|---|---|---| | doe-opendata | 14.432 | 13.925 | 1.029 | 12.534 | 14.426 | | service-bund (RSS) | 1.823 | 0 | 0 | 0 | 0 | Dieselbe Ausschreibung ergab aus zwei Quellen zwangsläufig zwei verschiedene Fingerabdrücke. Die Stufe konnte genau dort nicht greifen, wofür sie gebaut wurde. **Ein zweiter, unabhängiger Defekt kam beim Messen dazu:** Der Aktualisierungszweig von `TenderDedupService.resolve()` frischte bei geänderter Änderungssignatur Titel, Vergabestelle, Frist und Wert einer bestehenden Zeile auf — schrieb den Fingerabdruck aber nie mit. Der gespeicherte Wert driftete damit vom Inhalt der Zeile weg. Schon eine reine Neuberechnung ohne jede Formeländerung hätte 3 Gruppen zusammenfallen lassen. ## Warum CPV raus und der Wert nicht einfach mit Beide Felder sind quellasymmetrisch, aber sie verhalten sich bei echten Daten gegensätzlich. Von 26 Gruppen, die sich Vergabestelle, Titel und Frist teilen: - **21 Gruppen trennte bisher allein das CPV.** Angesehen zeigt sich immer dasselbe Muster: eine Zeile trägt die Sachdivision (34 Fahrzeuge, 45 Bau, 48 Software, 44 Material, 22 Druck, 71 Ingenieurwesen), die andere die Division **50** (Reparatur- und Wartungsdienste), teils ein bis zwei Tage später veröffentlicht. Das sind Erst- und Folgebekanntmachung derselben Sache — **21 übersehene Zusammenlegungen**, keine geretteten Trennungen. Dass die früheren Stufen sie nicht fangen, ist geprüft: die 42 Zeilen tragen 41 verschiedene OCIDs und 42 verschiedene Bekanntmachungsnummern. - **3 Gruppen trennte allein der Wert** — und die sind echt verschieden: zwei Lose desselben Bauvorhabens unter einem Titel (Neubau Radiologie mit 387.291 und 54.471; Sanierung Sport- und Mehrzweckhalle mit 200.000 und 80.000, beide CPV 71). Deshalb: CPV ersatzlos aus dem Hash, der Wert dagegen als **Veto nach dem Treffer**. Findet die Stufe einen Kandidaten und tragen beide Seiten einen Wert unterschiedlicher Größenordnung, wird nicht zusammengelegt. Fehlt der Wert auf einer Seite, schweigt das Veto. Das bleibt ein exakter Vergleich zweier Größenordnungsstufen — die Warnung im Dateikommentar gegen Ähnlichkeits- und Schwellenwertsuche steht unverändert. ## Ergebnis, am laufenden System gemessen | Messung | vorher | nachher | |---|---|---| | Fingerabdruck-Gruppen mit mehr als einer Zeile | 0 | **26** (52 Zeilen) | | davon durch das Wert-Veto getrennt | — | 2 | | Zeilen ohne die neue Versionsmarke | 16.255 | **0** | | API-Tests `src/tenders` | — | 374/374 grün (unabhängig nachgelaufen) | ### Die automatische Nachrechnung wurde scharf geprüft Die Formeländerung entwertet jeden gespeicherten Fingerabdruck. Damit das auf einer Bestandsdatenbank niemand von Hand nachziehen muss, trägt der Wert jetzt die Versionsmarke `v2:`, und ein Dienst rechnet beim Start alle Zeilen ohne diese Marke seitenweise nach. Geprüft wurde nicht nur, dass beim Start nichts passiert, sondern dass tatsächlich repariert wird: 500 Zeilen wurden künstlich auf ein altes Format zurückgesetzt, dann die API neu gebaut und gestartet. | Zeitpunkt | Beobachtung | |---|---| | Start 08:46:25 | Log: „Fingerabdruck-Nachrechnung: 500 Zeile(n) auf die neue Formel gebracht"; danach 0 veraltete Zeilen | | Neustart 08:46:49 | **keine** Meldung — der zweite Start ist folgenlos | ## Was bewusst offen bleibt Diese drei Grenzen stehen wörtlich im Dateikommentar von `tender-fingerprint.ts` und werden hier nicht weichgespült: 1. **Ein zweites Los gleicher Größenordnung wird weiterhin falsch zusammengelegt.** Das Veto trennt nur bei unterschiedlicher Größenordnung. Eines der drei gemessenen Los-Paare (117.647 gegen 386.554, beide in der Größenordnung 10⁵) fällt darunter. Bewusst getragen: eine falsche Zusammenlegung ist sichtbar und reparierbar, 21 übersehene waren der stille Totalausfall der Stufe. 2. **RSS und E-Mail-Alarm matchen nur noch über den Titel**, weil ihnen Vergabestelle und Frist fehlen. Im Normalfall heißt das, sie treffen weiterhin keine DÖE-Zeile. Fehlen der DÖE-Zeile aber ausnahmsweise beide Angaben auch, genügt der Titel. 3. **Genau ein solcher Fall liegt heute in der Datenbank:** Titel „LSA242", einmal über `service-bund` und einmal über `doe-opendata` eingegangen, beide am selben Tag und beide ohne Vergabestelle und Frist. Er wird nach dieser Änderung zusammengelegt. Ein Riegel dagegen wurde erwogen und verworfen: „keine Zusammenlegung ohne Vergabestelle und Frist" würde RSS dauerhaft von der Dublettenerkennung ausschließen, weil RSS diese Felder nie liefert. **Kein Rückwirken auf den Bestand:** Die Stufe entscheidet beim Einlesen, ob eine neue Zeile entsteht. Die jetzt sichtbaren 26 Gruppen bleiben als getrennte Zeilen bestehen; die Wirkung setzt beim nächsten Abruf ein. Wer den Altbestand zusammenführen will, braucht dafür einen eigenen Lauf — der ist hier nicht Teil des Fixes. ## Ausführung Quick-Task `260907-efh`, Commits `cb23e3b`, `9e5445e`, `ee3b28e`, `389a1fd`.