docs(13): revise plans per checker FLAG — SCHEMA-02 preserve + task order

- 13-03: dedup resolve() created=false branch preserves Phase-10 SCHEMA-02
  change detection (mutable fields + contentHash on changed re-poll); spec
  covers it. Prevents silent regression of DÖE re-poll updates.
- 13-01: reorder so fingerprint fn (Task 1) precedes fingerprint backfill
  (Task 2) — removes forward reference.
- 13-VALIDATION: task-id + coverage rows updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 08:31:27 +02:00
parent 74c00166e2
commit f3cd702197
3 changed files with 33 additions and 29 deletions
@@ -50,26 +50,8 @@ Output: schema.prisma (TenderSource + fingerprint), lokal angewendete Migration
<tasks>
<task type="auto">
<name>Task 1: TenderSource-Modell + fingerprint-Spalte + Migration + Backfill</name>
<files>apps/api/prisma/schema.prisma, apps/api/prisma/migrations/, apps/api/src/tenders/backfill-tender-source.ts</files>
<action>
Ergaenze in `schema.prisma` (per SCHEMA-03 / D-03, Referenz-Snippet in 13-RESEARCH.md "TenderSource-Modell (Prisma)"): neues `model TenderSource` mit id (uuid), tenderId, sourcePortal, sourceNoticeId, ocid (nullable), sourceUrl (nullable), createdAt, Relation `tender Tender @relation(fields:[tenderId], references:[id], onDelete: Cascade)`, `@@unique([sourcePortal, sourceNoticeId])` und `@@index([tenderId])`. Ergaenze in `model Tender`: `fingerprint String?` (nullable, additiv), `sources TenderSource[]`, `@@index([fingerprint])`. `dedupKey @unique` bleibt UNVERAENDERT (SCHEMA-02-Upsert-Target, D-01 — kein Drop in dieser Phase).
Erzeuge eine handgeschriebene Migration (Verzeichnis `apps/api/prisma/migrations/20260723120000_add_tender_source/migration.sql`) in ZWEI logischen Schritten in korrekter Reihenfolge (Pitfall 5): (a) CREATE TABLE "TenderSource" + ADD COLUMN "fingerprint" auf "Tender" + Indizes + Unique-Constraint; (b) Daten-Backfill `INSERT INTO "TenderSource" (...) SELECT gen_random_uuid(), t.id, t."sourcePortal", t."sourceNoticeId", t.ocid, t."sourceUrl", now() FROM "Tender" t;` (SQL-Skizze in 13-RESEARCH.md "Backfill-Migration"). Der Unique-Constraint wird NACH dem Backfill-Insert wirksam bzw. das Insert erzeugt keine Duplikate (DÖE-noticeIds sind live eindeutig).
Der `fingerprint`-Backfill der Bestands-Tender lebt NICHT in reinem SQL (Umlaut-/CPV-Normalisierung lebt im Code): schreibe ein einmaliges TS-Backfill-Script `backfill-tender-source.ts`, das alle Tender laedt, `tenderFingerprint(...)` (Task 2) aus title/buyerName/cpvDivisions/deadlineAt/estimatedValue berechnet und `Tender.fingerprint` per updateMany/Schleife setzt. estimatedValue ist `Decimal?` — in number/null konvertieren, bevor es an valueBucket geht.
Wende Migration lokal an (KEIN Docker-Deploy Testserver, MEMORY): `docker compose exec -T db psql -U tessera -d tessera_dev -f -` bzw. `pnpm --filter @tessera/api exec prisma migrate deploy` gegen die lokale DB; danach `pnpm --filter @tessera/api exec prisma generate`. DB-Name lokal verifizieren (tessera_dev laut Phase-12-Vorbild; Research nennt `tessera`) — vor dem Anwenden `\l` pruefen.
</action>
<verify>
<automated>cd apps/api && npx prisma validate && npx prisma generate</automated>
</verify>
<done>`model TenderSource` + `Tender.fingerprint` + `Tender.sources` existieren; `prisma validate` gruen; Migration lokal angewendet; `SELECT count(*) FROM "TenderSource"` == count der Tender (manuelle DB-Pruefung, siehe 13-VALIDATION.md).</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Pure NULL-tolerante Fingerprint-Funktion (SCHEMA-03, D-04)</name>
<name>Task 1: Pure NULL-tolerante Fingerprint-Funktion (SCHEMA-03, D-04)</name>
<files>apps/api/src/tenders/tender-fingerprint.ts, apps/api/src/tenders/tender-fingerprint.spec.ts</files>
<behavior>
- Gleicher Titel + Auftraggeber + CPV-Division, beide Wert=NULL und Frist=NULL -> gleicher Fingerprint (NULL-Toleranz, 92.4% NULL Wert / 15.7% NULL Frist live).
@@ -80,12 +62,30 @@ Wende Migration lokal an (KEIN Docker-Deploy Testserver, MEMORY): `docker compos
- Rueckgabe ist stabiler sha256-Hex-String (deterministisch ueber Laeufe).
</behavior>
<action>
Implementiere `tender-fingerprint.ts` als pure Funktion `tenderFingerprint(f: { buyerName: string|null; title: string; cpvDivisions: string[]; deadlineAt: Date|null; estimatedValue: number|null }): string` exakt nach 13-RESEARCH.md Pattern 4: interne Helfer `normText` (lowercase, ae/oe/ue/ss, non-alnum->space, trim), `cpvDivisionKey` (Set+sort+join), `valueBucket` (NULL->"", sonst `String(Math.floor(Math.log10(Math.max(v,1))))`), `deadlineKey` (Datum auf Tageskorn oder ""). Kanonischer String `[buyer, title, cpvDivKey, deadlineKey, valueBucket].join('|')` -> `createHash('sha256').update(...).digest('hex')` (Node crypto, kein Selbstbau, V6). KEIN Aehnlichkeits-Threshold (deterministisch, O(1)-Lookup). Schreibe die Tests RED-first gemaess behavior-Block, dann Implementierung bis gruen.
Diese Funktion MUSS vor dem Fingerprint-Backfill (Task 2) existieren — daher zuerst. Implementiere `tender-fingerprint.ts` als pure Funktion `tenderFingerprint(f: { buyerName: string|null; title: string; cpvDivisions: string[]; deadlineAt: Date|null; estimatedValue: number|null }): string` exakt nach 13-RESEARCH.md Pattern 4: interne Helfer `normText` (lowercase, ae/oe/ue/ss, non-alnum->space, trim), `cpvDivisionKey` (Set+sort+join), `valueBucket` (NULL->"", sonst `String(Math.floor(Math.log10(Math.max(v,1))))`), `deadlineKey` (Datum auf Tageskorn oder ""). Kanonischer String `[buyer, title, cpvDivKey, deadlineKey, valueBucket].join('|')` -> `createHash('sha256').update(...).digest('hex')` (Node crypto, kein Selbstbau, V6). KEIN Aehnlichkeits-Threshold (deterministisch, O(1)-Lookup). Schreibe die Tests RED-first gemaess behavior-Block, dann Implementierung bis gruen.
</action>
<verify>
<automated>pnpm --filter @tessera/api test -- tender-fingerprint</automated>
</verify>
<done>Alle behavior-Faelle gruen; Funktion ist pure (kein Prisma/IO-Import); NULL-Toleranz + Kollisions-Fall abgedeckt.</done>
<done>Alle behavior-Faelle gruen; Funktion ist pure (kein Prisma/IO-Import); NULL-Toleranz + Kollisions-Fall abgedeckt; steht fuer Task 2 zur Verfuegung.</done>
</task>
<task type="auto">
<name>Task 2: TenderSource-Modell + fingerprint-Spalte + Migration + Backfill</name>
<files>apps/api/prisma/schema.prisma, apps/api/prisma/migrations/, apps/api/src/tenders/backfill-tender-source.ts</files>
<action>
Ergaenze in `schema.prisma` (per SCHEMA-03 / D-03, Referenz-Snippet in 13-RESEARCH.md "TenderSource-Modell (Prisma)"): neues `model TenderSource` mit id (uuid), tenderId, sourcePortal, sourceNoticeId, ocid (nullable), sourceUrl (nullable), createdAt, Relation `tender Tender @relation(fields:[tenderId], references:[id], onDelete: Cascade)`, `@@unique([sourcePortal, sourceNoticeId])` und `@@index([tenderId])`. Ergaenze in `model Tender`: `fingerprint String?` (nullable, additiv), `sources TenderSource[]`, `@@index([fingerprint])`. `dedupKey @unique` bleibt UNVERAENDERT (SCHEMA-02-Upsert-Target, D-01 — kein Drop in dieser Phase).
Erzeuge eine handgeschriebene Migration (Verzeichnis `apps/api/prisma/migrations/20260723120000_add_tender_source/migration.sql`) in ZWEI logischen Schritten in korrekter Reihenfolge (Pitfall 5): (a) CREATE TABLE "TenderSource" + ADD COLUMN "fingerprint" auf "Tender" + Indizes + Unique-Constraint; (b) Daten-Backfill `INSERT INTO "TenderSource" (...) SELECT gen_random_uuid(), t.id, t."sourcePortal", t."sourceNoticeId", t.ocid, t."sourceUrl", now() FROM "Tender" t;` (SQL-Skizze in 13-RESEARCH.md "Backfill-Migration"). Der Unique-Constraint wird NACH dem Backfill-Insert wirksam bzw. das Insert erzeugt keine Duplikate (DÖE-noticeIds sind live eindeutig).
Der `fingerprint`-Backfill der Bestands-Tender lebt NICHT in reinem SQL (Umlaut-/CPV-Normalisierung lebt im Code): schreibe ein einmaliges TS-Backfill-Script `backfill-tender-source.ts`, das alle Tender laedt, die in Task 1 gebaute `tenderFingerprint(...)` aus title/buyerName/cpvDivisions/deadlineAt/estimatedValue berechnet und `Tender.fingerprint` per updateMany/Schleife setzt. estimatedValue ist `Decimal?` — in number/null konvertieren, bevor es an valueBucket geht.
Wende Migration lokal an (KEIN Docker-Deploy Testserver, MEMORY): `docker compose exec -T db psql -U tessera -d tessera_dev -f -` bzw. `pnpm --filter @tessera/api exec prisma migrate deploy` gegen die lokale DB; danach `pnpm --filter @tessera/api exec prisma generate`. DB-Name lokal verifizieren (tessera_dev laut Phase-12-Vorbild; Research nennt `tessera`) — vor dem Anwenden `\l` pruefen.
</action>
<verify>
<automated>cd apps/api && npx prisma validate && npx prisma generate</automated>
</verify>
<done>`model TenderSource` + `Tender.fingerprint` + `Tender.sources` existieren; `prisma validate` gruen; Migration lokal angewendet; fingerprint-Backfill nutzt die Task-1-Funktion; `SELECT count(*) FROM "TenderSource"` == count der Tender (manuelle DB-Pruefung, siehe 13-VALIDATION.md).</done>
</task>
<task type="auto">