| 13-scraping-adapters-cross-source-dedup |
01 |
database |
| prisma |
| postgresql |
| crypto |
| dedup |
| sha256 |
|
| phase |
provides |
| 10-doe-opendata-ingestion |
Tender model (sourcePortal, sourceNoticeId, ocid, dedupKey, cpvDivisions, contentHash), TenderNormalizerService, tender.types.ts contract |
|
|
| TenderSource Prisma model (1:n Tender, @@unique[sourcePortal, sourceNoticeId], onDelete Cascade) |
| Tender.fingerprint nullable column + index, additive alongside the unchanged dedupKey |
| pure tenderFingerprint() NULL-tolerant dedup-key function (title+buyer dominant, CPV division, value-bucket, deadline-day) |
| 2851 pre-existing DÖE tenders backfilled with one TenderSource row + a computed fingerprint each |
| SourceType widened to open union ('doe-opendata' | 'ai-netserver' | 'cosinex-dtvp') |
|
| 13-02-source-registry |
| 13-03-dedup-resolver |
| 13-04-netserver-adapter |
| 13-05-cosinex-adapter |
| 13-06-multi-source-display |
|
| added |
patterns |
|
|
| Pure fingerprint function (no I/O, no Prisma import) computed once and reused by both the one-time backfill script and (in Plan 13-03) the live dedup resolver |
| Two-step migration ordering for additive schema + data backfill: (1) table/column DDL, (2) SQL data copy, (3) unique constraint — avoids constraint violations from pre-existing data |
| One-time TS backfill scripts run via the compiled dist/ output (standard extensionless imports), not as raw .ts via node's native type-stripping — keeps tsc --noEmit clean project-wide |
|
|
| created |
modified |
| 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/prisma/migrations/20260723120000_add_tender_source/migration.sql |
|
| apps/api/prisma/schema.prisma |
| apps/api/src/tenders/tender.types.ts |
|
|
| fingerprint canonical string is [buyer, title, cpvDivisionKey, deadlineKey, valueBucket].join('|') -> sha256 hex, exactly per 13-RESEARCH Pattern 4 — deterministic O(1) lookup, no similarity threshold |
| dedupKey @unique on Tender is left untouched (SCHEMA-02 upsert target); fingerprint is a separate, additive, nullable column |
| One-time backfill script uses standard (extensionless) TS imports and documents running the compiled dist/ output, rather than node's raw .ts execution — Node 24's native type-stripping treats ESM-syntax .ts files as ESM requiring explicit .ts/.js extensions, which tsc rejects under moduleResolution:node; compiling first keeps both runtime execution and tsc --noEmit clean |
|
| Fuzzy-dedup fingerprint: pure function, NULL segments collapse to empty string rather than excluding the record, title always included as the collision guard |
|
|
| id |
description |
requirement |
verification |
human_judgment |
| D1 |
tenderFingerprint() is NULL-tolerant (title+buyer+CPV match with value/deadline both NULL), collision-resistant (different title -> different hash), umlaut-normalizing, order-independent on CPV divisions, magnitude-bucketed on value, and returns a stable sha256 hex string |
SCHEMA-03 |
| kind |
ref |
status |
| unit |
apps/api/src/tenders/tender-fingerprint.spec.ts (8 tests, all pass) |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D2 |
TenderSource model (1:n, @@unique[sourcePortal, sourceNoticeId]) and Tender.fingerprint exist in schema.prisma; migration applied locally; every pre-existing Tender has exactly one TenderSource row backfilled from its current sourcePortal/sourceNoticeId/sourceUrl/ocid |
SCHEMA-03 |
| kind |
ref |
status |
| integration |
npx prisma validate && npx prisma generate (clean) |
pass |
|
| kind |
ref |
status |
| manual_procedural |
docker compose exec -T db psql -U tessera -d tessera -c 'SELECT count(*) FROM "TenderSource"' -> 2851 == SELECT count(*) FROM "Tender" -> 2851 |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D3 |
Tender.fingerprint backfilled for all pre-existing rows using the Task-1 tenderFingerprint() function, with Decimal->number conversion for estimatedValue |
SCHEMA-03 |
| kind |
ref |
status |
| manual_procedural |
backfill-tender-source.ts run via compiled dist/ output — console output 'fingerprint set on 2851/2851 tenders', re-verified idempotent on second run; SELECT count(*) FROM "Tender" WHERE fingerprint IS NOT NULL -> 2851 |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D4 |
SourceType is an open union ('doe-opendata' | 'ai-netserver' | 'cosinex-dtvp') and NormalizedTenderFields carries an optional fingerprint field, without breaking the existing normalizer contract |
SCHEMA-03 |
| kind |
ref |
status |
| unit |
cd apps/api && npx tsc --noEmit -p tsconfig.json (clean) |
pass |
|
| kind |
ref |
status |
| unit |
pnpm --filter @tessera/api test (full suite, 234/234 pass) |
pass |
|
|
false |
|
|
35min |
2026-07-23 |
complete |