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
+1 -1
View File
@@ -605,7 +605,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
| 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 | | 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 |
| 11. Filter Engine, Results UI & Saved Searches | 6/6 | Complete | 2026-07-22 (Verifikation: passed) | | 11. Filter Engine, Results UI & Saved Searches | 6/6 | Complete | 2026-07-22 (Verifikation: passed) |
| 12. Tender Notifications | 4/4 | Complete | 2026-07-23 (Verifikation: passed) | | 12. Tender Notifications | 4/4 | Complete | 2026-07-23 (Verifikation: passed) |
| 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | Offener Befund | Alle Plaene fertig, Verifikation 3/4 — Fingerprint-Dedup greift wegen leerer cpvDivisions beim Scraper nicht (WINDOWS.md #11) | | 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | Complete | 2026-07-23, Dedup-Befund am 2026-09-07 behoben und am laufenden System gegengeprueft (13-DEDUP-FIX-2026-09-07.md) |
| 14. RSS, Email-Alert Ingestion & Module Rollout | 5/5 | Wartet auf Live-Test | Alle Plaene fertig, Verifikation 4/5 — echter Exchange/EWS-Durchlauf steht aus, bewusst zurueckgestellt (WINDOWS.md #12) | | 14. RSS, Email-Alert Ingestion & Module Rollout | 5/5 | Wartet auf Live-Test | Alle Plaene fertig, Verifikation 4/5 — echter Exchange/EWS-Durchlauf steht aus, bewusst zurueckgestellt (WINDOWS.md #12) |
| 15. Modul-Berechtigungen: Gruppen & User-Grants | 8/8 | Complete | 2026-08-04 (Bericht 15-04 am 2026-08-11 nachgezogen) | | 15. Modul-Berechtigungen: Gruppen & User-Grants | 8/8 | Complete | 2026-08-04 (Bericht 15-04 am 2026-08-11 nachgezogen) |
| 16. AD-Gruppen-Synchronisation | 5/5 | Complete | 2026-08-11 | | 16. AD-Gruppen-Synchronisation | 5/5 | Complete | 2026-08-11 |
+5 -5
View File
@@ -5,10 +5,10 @@ milestone_name: Ausschreibungs-Radar
current_phase: 17 current_phase: 17
current_phase_name: eigene-ausschreibungs-quellen-je-nutzer current_phase_name: eigene-ausschreibungs-quellen-je-nutzer
status: verified status: verified
stopped_at: "Phase 17 auf passed. Zusaetzlich die alten Browser-Gegenproben nachgeholt: WINDOWS #1, #2, #5 geschlossen, #3 durchgefuehrt. Dabei ein Defekt gefunden und als #10 erfasst — der Zugriffs-Guard greift nicht auf den modul-eigenen Routen (PERM-04 unvollstaendig). Offen bleiben #4 und #6 (brauchen echtes AD) sowie #10." stopped_at: "Phase 17 passed. Alte Gegenproben nachgeholt (#1,#2,#3,#5). Zwei Defekte behoben: #10 Zugriffs-Guard auf modul-eigenen Routen (Quick 260907-e8k), #11 Fingerabdruck-Dedup war wirkungslos, 0 statt 26 Gruppen (Quick 260907-efh). Beide im Browser bzw. am laufenden System gegengeprueft. Offen: #4 und #6 (brauchen echtes AD), #12 (Exchange-Live-Test)."
last_updated: "2026-09-07T08:10:00.000Z" last_updated: "2026-09-07T08:50:00.000Z"
last_activity: 2026-09-07 last_activity: 2026-09-07
last_activity_desc: Alle nachholbaren Browser-Gegenproben erledigt (Phase 15/16/17); ein Defekt gefunden (WINDOWS #10) last_activity_desc: WINDOWS #10 und #11 behoben und gegengeprueft; Ledger auf 3 offene Punkte
progress: progress:
total_phases: 17 total_phases: 17
completed_phases: 17 completed_phases: 17
@@ -336,7 +336,7 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity ## Session Continuity
Last session: 2026-09-07T08:10:00.000Z Last session: 2026-09-07T08:50:00.000Z
Stopped at: Naechster Schritt: WINDOWS #10 beheben (Guard ins gemeinsame Modul-Layout) oder /gsd-ship fuer Phase 17. Stopped at: Alle drei verbleibenden Ledger-Punkte brauchen Fremdsysteme (AD, Exchange). Naechster moeglicher Schritt: /gsd-ship.
Resume file: None Resume file: None
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
@@ -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`.