Files
schalli 389a1fd717 docs(260907-efh): complete Cross-Source-Dedup-Fingerabdruck-Fix
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
2026-09-07 10:44:39 +02:00

13 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions requirements-completed coverage duration completed status
quick-260907-efh 01 api
dedup
fingerprint
tenders
prisma
nestjs
phase provides
13-scraping-adapters-cross-source-dedup TenderDedupService three-tier resolver, tenderFingerprint() v1, backfill-tender-source.ts
tenderFingerprint() v2 (buyerName, title, deadlineAt only) with FINGERPRINT_VERSION_PREFIX
valueBucketsContradict() exported veto function
Value veto wired into TenderDedupService's fingerprint tier
Fingerprint refresh on TenderDedupService's change-detection update branch
TenderFingerprintBackfillService: automatic startup recompute of outdated fingerprints
13-scraping-adapters-cross-source-dedup
any future tender dedup work
tokens tasks commits
9035 3 3
added patterns
Version-prefixed hash output (FINGERPRINT_VERSION_PREFIX) so a startup backfill can detect stale rows without a separate schema flag
Post-match veto function applied after a fingerprint hit, kept separate from the hash itself (exact bucket comparison, no similarity/threshold logic)
created modified
apps/api/src/tenders/tender-fingerprint-backfill.service.ts
apps/api/src/tenders/tender-fingerprint-backfill.service.spec.ts
apps/api/src/tenders/tender-fingerprint.ts
apps/api/src/tenders/tender-fingerprint.spec.ts
apps/api/src/tenders/backfill-tender-source.ts
apps/api/src/tenders/tender-dedup.service.ts
apps/api/src/tenders/tender-dedup.service.spec.ts
apps/api/src/tenders/tenders.module.ts
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 SCHEMA-03
kind ref status
unit apps/api/src/tenders/tender-fingerprint.spec.ts#valueBucketsContradict pass
kind ref status
unit apps/api/src/tenders/tender-dedup.service.spec.ts#Stufe-3-Wertveto (WINDOWS.md #11) pass
false
id description requirement verification human_judgment
D3 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:

  1. 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.
  2. 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.
  3. 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:

  1. 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
  2. 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
  3. 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/tender-fingerprint-backfill.service.ts - NEU: automatische Startup-Nachrechnung
  • apps/api/src/tenders/tender-fingerprint-backfill.service.spec.ts - NEU: vier Tests (leere DB, alter+leerer Hash, zweiter Lauf no-op, DB-Fehler geschluckt)
  • 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).


Phase: quick-260907-efh Completed: 2026-09-07