WINDOWS.md #11 als fixed markiert — Fingerprint-Stufe hasht nur noch
[buyerName, title, deadlineAt], Wert kehrt als exakter Widerspruchs-Veto
zurueck, 16.255 Bestandszeilen wurden gegen die lokale DB nachgerechnet
(0 -> 26 Duplikat-Gruppen, davon 2 durch den Veto geschuetzt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5jtbGzC5Sf9npJ3JCjKhq
CPV and estimatedValue removed from the hash with no replacement inside the hash — CPV is not compared anywhere anymore.
estimatedValue returns as a post-match veto (valueBucketsContradict), not a hash input — exact order-of-magnitude comparison, never a similarity threshold.
TenderDedupService's change-detection update branch now writes fingerprint alongside the other refreshed fields, fixing a second, independently discovered defect (fingerprint drift on update).
Startup backfill (TenderFingerprintBackfillService) recomputes only rows failing the FINGERPRINT_VERSION_PREFIX check, paged at 500 rows, so a second start is a no-op.
SCHEMA-03
id
description
requirement
verification
human_judgment
D1
DOE-shaped and scraper-shaped records of the same tender (same buyer/title/deadline, differing CPV/value) produce the same v2 fingerprint
SCHEMA-03
kind
ref
status
unit
apps/api/src/tenders/tender-fingerprint.spec.ts#is the Kernregression this plan exists for
pass
kind
ref
status
unit
apps/api/src/tenders/tender-fingerprint.spec.ts#gold value
pass
false
id
description
requirement
verification
human_judgment
D2
A fingerprint hit is vetoed when both sides carry an estimatedValue of a different order of magnitude; not vetoed when either side is null or both share the same magnitude
The change-detection update branch writes the recomputed fingerprint alongside the other refreshed fields, closing the drift bug found while measuring
SCHEMA-03
kind
ref
status
unit
apps/api/src/tenders/tender-dedup.service.spec.ts#OCID match with a changed contentHash still updates the Tender mutable fields
pass
false
id
description
requirement
verification
human_judgment
D4
Pre-existing Tender rows are recomputed to the v2 fingerprint automatically on startup; a second startup does no work
SCHEMA-03
kind
ref
status
unit
apps/api/src/tenders/tender-fingerprint-backfill.service.spec.ts (4 tests: empty DB, old+null hash both caught, second run no-op, DB error swallowed)
pass
kind
ref
status
other
Real local-DB recompute via backfill-tender-source.js (build + node dist/tenders/backfill-tender-source.js against tessera-ctl-db-1) — measured numbers below
pass
false
id
description
verification
human_judgment
rationale
D5
Next local API restart performs no further recompute (log stays silent about the backfill)
true
The plan marks this a human-check step — the user restarts the local API themselves; the executor cannot restart it and observe the log itself under this task's constraints.
30min
2026-09-07
complete
Phase quick-260907-efh Plan 01: Cross-Source-Dedup-Fingerabdruck korrigiert Summary
Fingerabdruck-Formel auf [Vergabestelle, Titel, Frist] verengt, Wert kehrt als exakter Widerspruchs-Veto zurueck, 16.255 Bestandszeilen automatisch nachgerechnet — 26 statt 0 Duplikat-Gruppen gefunden, davon 2 durch den Wert-Veto vor Fehlzusammenlegung geschuetzt.
Performance
Duration: ca. 30 min
Tasks: 3/3 completed
Files modified: 6 modified, 2 created
Accomplishments
tenderFingerprint() hasht nur noch drei statt fuenf Segmente (buyerName, title, deadlineAt) — CPV-Division und geschaetzter Wert sind vollstaendig aus dem Hash entfernt, weil sie an der Live-Datenbank gemessen systematisch nur von einer Quellenart geliefert werden und die Stufe damit fuer ihren eigentlichen Zweck (Cross-Source-Match) blind machten.
Neue Versionsmarke FINGERPRINT_VERSION_PREFIX = "v2:" vor dem Hash — macht veraltete Zeilen maschinell erkennbar, ohne ein zusaetzliches Schemafeld.
Neue, exportierte Funktion valueBucketsContradict(): exakter Groessenordnungsvergleich, angewendet als Veto NACH einem Fingerprint-Treffer in TenderDedupService, nie als Bestandteil des Hashes.
Beim Messen ein zweiter, unabhaengiger Defekt gefunden und mit repariert: der Aktualisierungszweig von TenderDedupService.resolve() aktualisierte Titel/Vergabestelle/Frist einer bestehenden Zeile, schrieb den Fingerabdruck dabei aber nie mit — der gespeicherte Wert driftete vom Inhalt der Zeile weg. Jetzt wird der einmal berechnete Fingerabdruck in beiden Zweigen (Anlegen und Aktualisieren) geschrieben.
Neuer Dienst TenderFingerprintBackfillService (OnApplicationBootstrap, gleiche Bauform wie die LDAP-Bind-Passwort-Nachverschluesselung): rechnet beim Start seitenweise (500 Zeilen pro Transaktion) alle Zeilen ohne die neue Versionsmarke nach; ein zweiter Start tut nichts mehr. In tenders.module.ts bewusst VOR TenderSchedulerService eingetragen.
Reale Nachrechnung gegen die lokale Datenbank gefahren (nicht nur behauptet) — Zahlen unten.
Gemessene Zahlen (real gegen die lokale Datenbank gefahren, backfill-tender-source.js)
Messung
Wert
Zeilen in Tender
16.255
Fingerabdruck-Gruppen mit mehr als einer Zeile, VOR der Nachrechnung
0
Fingerabdruck-Gruppen mit mehr als einer Zeile, NACH der Nachrechnung
26
Zeilen ohne die neue Versionsmarke NACH der Nachrechnung
0
Gruppen davon mit widersprechenden Wert-Groessenordnungen (Veto greift)
2
Diese Zahlen stimmen exakt mit den in der Aufgabenstellung vorab gemessenen Werten ueberein (erwartet: 26 Gruppen, 2 Wertkonflikte).
Die drei verbleibenden, bewusst getragenen Grenzen
Woertlich im Dateikommentar von apps/api/src/tenders/tender-fingerprint.ts UND hier festgehalten:
Ein zweites Los gleicher Groessenordnung wird faelschlich zusammengelegt. Zwei Lose desselben Bauvorhabens mit Werten derselben Groessenordnung teilen sich Titel, Vergabestelle und Frist und werden vom Wert-Veto nicht getrennt — der Veto greift nur bei UNTERSCHIEDLICHER Groessenordnung. An der Live-Datenbank gemessen betrifft das eines der drei urspruenglich gefundenen Los-Paare (117.647 gegen 386.554 — beide Groessenordnung 10^5). Bewusst getragen: eine falsche Zusammenlegung ist sichtbar und reparierbar, die vorherigen 21 uebersehenen Duplikate waren der stille Totalausfall der Stufe.
Quellen ohne Vergabestelle und ohne Frist (RSS, E-Mail-Alarm) matchen nur noch ueber den Titel. Im Normalfall bedeutet das: sie treffen weiterhin keine DOE-Zeile (DOE traegt fast immer beides). Fehlt aber ausnahmsweise auch der DOE-Zeile Vergabestelle und Frist, reicht der Titel allein zur Zusammenlegung.
Genau ein solcher Fall existiert heute schon gemessen in der Datenbank: Titel "LSA242", einmal ueber service-bund, einmal ueber doe-opendata eingegangen — wird nach dieser Aenderung zusammengelegt.
Zusaetzlich unveraendert gueltig, aus dem Dateikommentar uebernommen: der Wertvergleich bleibt ein exakter Vergleich zweier Groessenordnungs-Stufen, keine Aehnlichkeits- oder Schwellenwertsuche — das alte Verbot dazu ist NICHT aufgeweicht worden.
Task Commits
Jede Aufgabe wurde einzeln committet:
Task 1: Hash-Formel verengen, Versionsmarke, Wert-Widerspruch als eigene Funktion — cb23e3b (fix, tdd) — tender-fingerprint.ts, tender-fingerprint.spec.ts, backfill-tender-source.ts
Task 2: Wert-Veto in Stufe 3 des Resolvers, Fingerabdruck bei Update mitschreiben — 9e5445e (fix, tdd) — tender-dedup.service.ts, tender-dedup.service.spec.ts
Task 3: Automatische Nachrechnung der Bestandszeilen beim Start — ee3b28e (feat) — tender-fingerprint-backfill.service.ts, tender-fingerprint-backfill.service.spec.ts, tenders.module.ts
Hinweis zur Commit-Zaehlung: zwischen Task 2 und Task 3 landete auf main ein fremder Commit (0645cac, "docs(15): WINDOWS #10 geschlossen") aus einer parallel laufenden, unabhaengigen Aufgabe (Scope: apps/web/) — nicht Teil dieses Plans, nicht angefasst. Der Ausfuehrungsmodus dieser Aufgabe war SEQUENTIAL/ohne Worktree auf dem geteilten main-Branch, daher taucht dieser Commit zwischen den eigenen Commits im Log auf.
Files Created/Modified
apps/api/src/tenders/tender-fingerprint.ts - v2-Formel, FINGERPRINT_VERSION_PREFIX, valueBucketsContradict(), ehrlicher Dateikommentar mit den drei Grenzen
apps/api/src/tenders/tender-fingerprint.spec.ts - Tests auf v2-Verhalten umgeschrieben, inkl. handgerechnetem Goldwert-Test
apps/api/src/tenders/backfill-tender-source.ts - Aufruf/Feldauswahl an neue Signatur angepasst, decimalToNumberOrNull entfernt (nicht mehr gebraucht)
apps/api/src/tenders/tender-dedup.service.ts - Fingerabdruck einmal berechnet, in Anlege- UND Aktualisierungszweig geschrieben; Wert-Veto in Stufe 3
apps/api/src/tenders/tender-dedup.service.spec.ts - vier neue Wert-Veto-Tests, bestehender Update-Test um Fingerabdruck-Erwartung erweitert
apps/api/src/tenders/tenders.module.ts - TenderFingerprintBackfillService als Provider, mit Kommentar VOR TenderSchedulerService platziert
Decisions Made
CPV bleibt vollstaendig aus dem Fingerabdruck draussen — keine Ersatzformel, keine Wiedereinfuehrung an anderer Stelle.
Der Wertvergleich lebt bewusst in tender-fingerprint.ts neben der Groessenordnungsbildung, damit es genau eine Definition von "Groessenordnung" im Code gibt.
Der Aktualisierungszweig in TenderDedupService schreibt weiterhin niemals ownerTenantId — dieser zweite, beim Messen gefundene Defekt wurde bewusst NICHT mitrepariert, weil er ausserhalb der Aufgabenstellung dieses Plans liegt (siehe Threat Register T-EFH-02, "accept").
Deviations from Plan
None - plan executed exactly as written. Der im Objective bereits angekuendigte "zweite Defekt" (Fingerabdruck-Drift im Aktualisierungszweig) war explizit Teil von Task 2 der Aufgabenstellung, keine ungeplante Abweichung.
Issues Encountered
None.
User Setup Required
None - keine externe Konfiguration noetig. Ein Punkt bleibt beim Nutzer: beim naechsten Neustart der lokalen API selbst pruefen, dass im Log KEINE Meldung ueber nachgerechnete Fingerabdruecke mehr erscheint (die lokale Datenbank wurde in diesem Lauf bereits auf v2 umgestellt — ein Log-Eintrag beim naechsten Start waere ein Zeichen, dass die Veraltet-Bedingung falsch herum greift).
Next Phase Readiness
WINDOWS.md #11 ist als fixed markiert (gsd-tools windows fixed 11). Die in 13-VERIFICATION.md offene Wahrheit 3 ist nicht mehr durch die Hash-Formel blockiert; die dort noch offene Live-Gegenprobe mit einer zweiten tatsaechlich aktiven Scraper-Quelle bleibt unveraendert offen (dedupActive erfordert activePortalCount >= 2 — aktuell laeuft praktisch nur DOE aktiv).