diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 8b3ae4d..f80fe77 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 | | 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) | -| 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) | | 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 | diff --git a/.planning/STATE.md b/.planning/STATE.md index 6b8f1ac..9e5e099 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,10 +5,10 @@ milestone_name: Ausschreibungs-Radar current_phase: 17 current_phase_name: eigene-ausschreibungs-quellen-je-nutzer 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." -last_updated: "2026-09-07T08:10:00.000Z" +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:50:00.000Z" 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: total_phases: 17 completed_phases: 17 @@ -336,7 +336,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-07T08:10:00.000Z -Stopped at: Naechster Schritt: WINDOWS #10 beheben (Guard ins gemeinsame Modul-Layout) oder /gsd-ship fuer Phase 17. +Last session: 2026-09-07T08:50:00.000Z +Stopped at: Alle drei verbleibenden Ledger-Punkte brauchen Fremdsysteme (AD, Exchange). Naechster moeglicher Schritt: /gsd-ship. 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 diff --git a/.planning/phases/13-scraping-adapters-cross-source-dedup/13-DEDUP-FIX-2026-09-07.md b/.planning/phases/13-scraping-adapters-cross-source-dedup/13-DEDUP-FIX-2026-09-07.md new file mode 100644 index 0000000..702bcec --- /dev/null +++ b/.planning/phases/13-scraping-adapters-cross-source-dedup/13-DEDUP-FIX-2026-09-07.md @@ -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`.