docs(13): Dedup-Fix dokumentiert und am laufenden System gegengeprueft
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 48s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m1s

Die Fingerabdruck-Stufe legte vor dem Fix bei 16.255 Zeilen null Duplikate
zusammen — nicht zu wenige, sondern gar keine. Nach der Umstellung auf die
quellstabilen Felder sind es 26 Gruppen mit 52 Zeilen, davon 2 durch das neue
Wert-Veto getrennt. 374/374 API-Tests unabhaengig nachgelaufen.

Die automatische Nachrechnung wurde scharf geprueft statt nur beobachtet: 500
Zeilen wurden kuenstlich auf ein altes Fingerabdruck-Format zurueckgesetzt, dann
die API neu gebaut und gestartet. Der Start um 08:46:25 meldete "500 Zeile(n) auf
die neue Formel gebracht" und hinterliess null veraltete Zeilen; der Neustart um
08:46:49 erzeugte keine Meldung mehr. Damit ist beides belegt — dass sie repariert
und dass sie nicht bei jedem Start erneut laeuft.

Der Bericht haelt die drei bewusst getragenen Grenzen fest (zweites Los gleicher
Groessenordnung wird falsch zusammengelegt; RSS/E-Mail matchen nur ueber den Titel;
der eine bereits messbare Fall LSA242) und begruendet, warum ein Riegel dagegen
verworfen wurde: er wuerde RSS dauerhaft von der Dublettenerkennung ausschliessen.
Ausserdem festgehalten, dass die Stufe nur beim Einlesen wirkt — der Altbestand
wird nicht rueckwirkend zusammengefuehrt.

Phase 13 steht damit auf Complete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
This commit is contained in:
2026-09-07 10:48:29 +02:00
parent 389a1fd717
commit dba261de38
3 changed files with 132 additions and 6 deletions
@@ -0,0 +1,126 @@
---
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`.