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:
@@ -50,26 +50,8 @@ Output: schema.prisma (TenderSource + fingerprint), lokal angewendete Migration
|
|||||||
|
|
||||||
<tasks>
|
<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">
|
<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>
|
<files>apps/api/src/tenders/tender-fingerprint.ts, apps/api/src/tenders/tender-fingerprint.spec.ts</files>
|
||||||
<behavior>
|
<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).
|
- 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).
|
- Rueckgabe ist stabiler sha256-Hex-String (deterministisch ueber Laeufe).
|
||||||
</behavior>
|
</behavior>
|
||||||
<action>
|
<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>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>pnpm --filter @tessera/api test -- tender-fingerprint</automated>
|
<automated>pnpm --filter @tessera/api test -- tender-fingerprint</automated>
|
||||||
</verify>
|
</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>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ must_haves:
|
|||||||
- "Mit nur DÖE aktiv (activePortalCount < 2) laeuft die Fingerprint-Stufe NIE — zwei Tender mit gleichem Fingerprint bleiben zwei Tender (D-05, Erfolgskriterium 3, inert)."
|
- "Mit nur DÖE aktiv (activePortalCount < 2) laeuft die Fingerprint-Stufe NIE — zwei Tender mit gleichem Fingerprint bleiben zwei Tender (D-05, Erfolgskriterium 3, inert)."
|
||||||
- "Ab der 2. aktiven Quelle haengt ein Fingerprint-Match eine zusaetzliche TenderSource an den bestehenden Tender an, statt einen neuen Tender zu erzeugen (D-03/D-04)."
|
- "Ab der 2. aktiven Quelle haengt ein Fingerprint-Match eine zusaetzliche TenderSource an den bestehenden Tender an, statt einen neuen Tender zu erzeugen (D-03/D-04)."
|
||||||
- "pollDueSources faechert ueber ALLE aktiven Configs (findMany isActive), mit catch-per-source (eine kaputte Quelle killt den Tick nicht, D-01)."
|
- "pollDueSources faechert ueber ALLE aktiven Configs (findMany isActive), mit catch-per-source (eine kaputte Quelle killt den Tick nicht, D-01)."
|
||||||
|
- "SCHEMA-02-Change-Detection bleibt erhalten: ein Re-Poll mit geaendertem contentHash aktualisiert die mutable Tender-Felder weiterhin (kein Phase-10-Regress) — die Logik wandert vom direkten upsert in resolve()."
|
||||||
artifacts:
|
artifacts:
|
||||||
- apps/api/src/tenders/tender-dedup.service.ts
|
- apps/api/src/tenders/tender-dedup.service.ts
|
||||||
key_links:
|
key_links:
|
||||||
@@ -59,18 +60,21 @@ Output: tender-dedup.service.ts (+ spec), erweitertes pollDueSources (+ spec), M
|
|||||||
- dedupActive=true, OCID-Match: haengt TenderSource an bestehenden Tender, created=false.
|
- dedupActive=true, OCID-Match: haengt TenderSource an bestehenden Tender, created=false.
|
||||||
- dedupActive=true, kein OCID aber source:noticeId bereits vorhanden (idempotenter Re-Poll): upsert der TenderSource, kein neuer Tender.
|
- dedupActive=true, kein OCID aber source:noticeId bereits vorhanden (idempotenter Re-Poll): upsert der TenderSource, kein neuer Tender.
|
||||||
- dedupActive=true, kein OCID/noticeId-Match aber Fingerprint-Match: haengt zusaetzliche TenderSource an, created=false (D-03 Merge).
|
- dedupActive=true, kein OCID/noticeId-Match aber Fingerprint-Match: haengt zusaetzliche TenderSource an, created=false (D-03 Merge).
|
||||||
|
- SCHEMA-02-Erhalt (created=false, Re-Poll derselben Quelle): wenn sich der contentHash gegenueber dem Bestands-Tender geaendert hat, werden dessen mutable Felder (title, buyerName, cpvCodes, cpvDivisions, region, plz, bundesland, deadlineAt, estimatedValue, procedureType, sourceUrl, contentHash) aktualisiert — exakt die Phase-10-SCHEMA-02-Change-Detection. Ein DÖE-Re-Poll mit geaenderter Frist/Titel MUSS den Tender-Kern weiter aktualisieren (kein Regress).
|
||||||
- Kein Match: tender.create (inkl. fingerprint) + tenderSource.create, created=true.
|
- Kein Match: tender.create (inkl. fingerprint) + tenderSource.create, created=true.
|
||||||
- tenderSource.upsert nutzt @@unique([sourcePortal, sourceNoticeId]) als where-Target.
|
- tenderSource.upsert nutzt @@unique([sourcePortal, sourceNoticeId]) als where-Target.
|
||||||
</behavior>
|
</behavior>
|
||||||
<action>
|
<action>
|
||||||
Implementiere `tender-dedup.service.ts` (`@Injectable`) nach 13-RESEARCH.md Pattern 5: Methode `resolve(n: NormalizedTenderFields, opts: { dedupActive: boolean }): Promise<{ tenderId: string; created: boolean }>`. Reihenfolge: (1) OCID-Match `prisma.tender.findFirst({ where: { ocid } })` falls ocid vorhanden; (2) `prisma.tenderSource.findUnique({ where: { sourcePortal_sourceNoticeId: {...} } })` -> zugehoerigen Tender; (3) NUR wenn `opts.dedupActive` und noch kein Match: `prisma.tender.findFirst({ where: { fingerprint: tenderFingerprint(n) } })`. Bei Match: `tenderSource.upsert` (create: an match.id haengen, update: sourceUrl) und `created:false`. Ohne Match: `tender.create({ data: { ...felder, fingerprint: tenderFingerprint(n) } })` + `tenderSource.create`, `created:true`. Der `dedupActive`-Gate ist der D-05-Kern — Stufe 3 wird bei nur einer Quelle NIE erreicht.
|
Implementiere `tender-dedup.service.ts` (`@Injectable`) nach 13-RESEARCH.md Pattern 5: Methode `resolve(n: NormalizedTenderFields, opts: { dedupActive: boolean }): Promise<{ tenderId: string; created: boolean }>`. Reihenfolge: (1) OCID-Match `prisma.tender.findFirst({ where: { ocid } })` falls ocid vorhanden; (2) `prisma.tenderSource.findUnique({ where: { sourcePortal_sourceNoticeId: {...} } })` -> zugehoerigen Tender; (3) NUR wenn `opts.dedupActive` und noch kein Match: `prisma.tender.findFirst({ where: { fingerprint: tenderFingerprint(n) } })`.
|
||||||
|
|
||||||
WICHTIG (T-10-09 / Phase-10-Muster): plain PrismaService, KEIN forTenant()/RLS — Tender/TenderSource sind plattform-global. Schreibe die Spec RED-first mit gemocktem PrismaService (kein echtes IO) nach behavior-Block; der inert-Fall (dedupActive=false -> zwei Tender) ist der D-05-Beweis.
|
Bei Match (`created:false`): `tenderSource.upsert` (create: an match.id haengen, update: sourceUrl). ZUSAETZLICH — SCHEMA-02-Change-Detection ERHALTEN (Regress-Vermeidung): der bestehende Phase-10-Update-Pfad von `pollDueSources` (das UPDATE des `tender.upsert`) darf NICHT verloren gehen. Wenn `n.contentHash` vom contentHash des Matches abweicht, `prisma.tender.update({ where: { id: match.id }, data: { title, buyerName, cpvCodes, cpvDivisions, region, plz, bundesland, deadlineAt, estimatedValue, procedureType, sourceUrl, contentHash } })` (exakt die mutable-Feldliste aus dem heutigen `tender-ingestion.service.ts`-UPDATE-Block). So aktualisiert ein DÖE-Re-Poll mit geaenderter Frist/Titel/contentHash den Tender-Kern weiterhin — nur die create-vs-update-Entscheidung wandert vom direkten upsert in den Resolver, die SCHEMA-02-Semantik bleibt identisch. Ohne Match (`created:true`): `tender.create({ data: { ...felder, fingerprint: tenderFingerprint(n) } })` + `tenderSource.create`. Der `dedupActive`-Gate ist der D-05-Kern — Stufe 3 wird bei nur einer Quelle NIE erreicht.
|
||||||
|
|
||||||
|
WICHTIG (T-10-09 / Phase-10-Muster): plain PrismaService, KEIN forTenant()/RLS — Tender/TenderSource sind plattform-global. Schreibe die Spec RED-first mit gemocktem PrismaService (kein echtes IO) nach behavior-Block; der inert-Fall (dedupActive=false -> zwei Tender) ist der D-05-Beweis; ein expliziter Test deckt den SCHEMA-02-Erhalt (Match mit geaendertem contentHash -> tender.update der mutable Felder).
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>pnpm --filter @tessera/api test -- tender-dedup</automated>
|
<automated>pnpm --filter @tessera/api test -- tender-dedup</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<done>Alle behavior-Faelle gruen; inert bei einer Quelle, Merge ab zwei Quellen; nutzt tenderFingerprint aus 13-01.</done>
|
<done>Alle behavior-Faelle gruen; inert bei einer Quelle, Merge ab zwei Quellen; nutzt tenderFingerprint aus 13-01; SCHEMA-02-Change-Detection bleibt erhalten (Match+geaenderter contentHash -> tender.update, getestet).</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
@@ -79,7 +83,7 @@ WICHTIG (T-10-09 / Phase-10-Muster): plain PrismaService, KEIN forTenant()/RLS
|
|||||||
<action>
|
<action>
|
||||||
Baue `pollDueSources` von `findUnique({ sourceType:'doe-opendata' })` auf Fan-out um (13-RESEARCH.md Pattern 3): `const configs = await prisma.tenderSourcePollConfig.findMany({ where: { isActive: true } })`; `activePortalCount = configs.length`; `dedupActive = activePortalCount >= 2` (D-05). Iteriere die Configs: `const adapter = registry.get(config.sourceType as SourceType); if (!adapter) continue;` — je Config in `try { ... } catch (err) { this.logger.error(...) }` (catch-per-source, D-01: eine kaputte Quelle killt die anderen nicht). Der bestehende Day-Cursor-Gate (`nextDayToFetch`) + fetch + normalize bleiben PRO Quelle erhalten; ersetze den direkten `tender.upsert`-Block durch `dedup.resolve(normalized, { dedupActive })` und sammle `created`-Tender-IDs (Delta-Boundary D-07 bleibt: nur genuin neue IDs an `matching.matchDelta`). lastIngestedDay-Advance bleibt pro Config (Cursor je sourceType). Injiziere `SourceRegistry` und `TenderDedupService` in den Constructor; entferne die direkte `DoeOpenDataAdapter`-Nutzung im Poll-Pfad zugunsten `registry.get('doe-opendata')`.
|
Baue `pollDueSources` von `findUnique({ sourceType:'doe-opendata' })` auf Fan-out um (13-RESEARCH.md Pattern 3): `const configs = await prisma.tenderSourcePollConfig.findMany({ where: { isActive: true } })`; `activePortalCount = configs.length`; `dedupActive = activePortalCount >= 2` (D-05). Iteriere die Configs: `const adapter = registry.get(config.sourceType as SourceType); if (!adapter) continue;` — je Config in `try { ... } catch (err) { this.logger.error(...) }` (catch-per-source, D-01: eine kaputte Quelle killt die anderen nicht). Der bestehende Day-Cursor-Gate (`nextDayToFetch`) + fetch + normalize bleiben PRO Quelle erhalten; ersetze den direkten `tender.upsert`-Block durch `dedup.resolve(normalized, { dedupActive })` und sammle `created`-Tender-IDs (Delta-Boundary D-07 bleibt: nur genuin neue IDs an `matching.matchDelta`). lastIngestedDay-Advance bleibt pro Config (Cursor je sourceType). Injiziere `SourceRegistry` und `TenderDedupService` in den Constructor; entferne die direkte `DoeOpenDataAdapter`-Nutzung im Poll-Pfad zugunsten `registry.get('doe-opendata')`.
|
||||||
|
|
||||||
Erweitere die bestehende `tender-ingestion.service.spec.ts`: (a) fan-out ueber mehrere aktive Configs; (b) catch-per-source (Quelle A wirft -> Quelle B laeuft trotzdem, Tick wirft nicht); (c) dedupActive-Gate (1 Config -> resolve mit dedupActive=false; 2 Configs -> dedupActive=true); (d) matchDelta bekommt nur created-IDs. Registry/Dedup im Test mocken.
|
Erweitere die bestehende `tender-ingestion.service.spec.ts`: (a) fan-out ueber mehrere aktive Configs; (b) catch-per-source (Quelle A wirft -> Quelle B laeuft trotzdem, Tick wirft nicht); (c) dedupActive-Gate (1 Config -> resolve mit dedupActive=false; 2 Configs -> dedupActive=true); (d) matchDelta bekommt nur created-IDs. Bestehende SCHEMA-02-Re-Poll-Tests muessen gruen bleiben (Change-Detection lebt jetzt in resolve, nicht mehr im direkten upsert) — falls ein Test den upsert-UPDATE direkt assertete, auf den resolve-Pfad umstellen. Registry/Dedup im Test mocken.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>pnpm --filter @tessera/api test -- tender-ingestion.service</automated>
|
<automated>pnpm --filter @tessera/api test -- tender-ingestion.service</automated>
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ Ein neues Package: `node-html-parser` (oder `cheerio`) — NUR nach blockierende
|
|||||||
|
|
||||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||||
| 13-01-01 | 01 | 1 | SCHEMA-03 | T-13-01-01/03 | TenderSource (1:n, @@unique[sourcePortal,sourceNoticeId]) + Tender.fingerprint additiv; 2-Schritt-Migration + Backfill lokal | integration | `cd apps/api && npx prisma validate && npx prisma generate` (+ psql-Count, manual) | ⚠️ migration | ⬜ pending |
|
| 13-01-01 | 01 | 1 | SCHEMA-03 | — | tenderFingerprint pure, NULL-tolerant (Wert/Frist NULL -> Match), kollisionsarm (Titel im Hash), sha256; steht vor Backfill bereit | unit | `pnpm --filter @tessera/api test -- tender-fingerprint` | ❌ W0 | ⬜ pending |
|
||||||
| 13-01-02 | 01 | 1 | SCHEMA-03 | — | tenderFingerprint pure, NULL-tolerant (Wert/Frist NULL -> Match), kollisionsarm (Titel im Hash), sha256 | unit | `pnpm --filter @tessera/api test -- tender-fingerprint` | ❌ W0 | ⬜ pending |
|
| 13-01-02 | 01 | 1 | SCHEMA-03 | T-13-01-01/03 | TenderSource (1:n, @@unique[sourcePortal,sourceNoticeId]) + Tender.fingerprint additiv; 2-Schritt-Migration + Backfill lokal (nutzt Task-1-fn) | integration | `cd apps/api && npx prisma validate && npx prisma generate` (+ psql-Count, manual) | ⚠️ migration | ⬜ pending |
|
||||||
| 13-01-03 | 01 | 1 | SCHEMA-03 | — | SourceType-Union offen + NormalizedTenderFields.fingerprint; typecheck | typecheck | `cd apps/api && npx tsc --noEmit -p tsconfig.json` | n/a | ⬜ pending |
|
| 13-01-03 | 01 | 1 | SCHEMA-03 | — | SourceType-Union offen + NormalizedTenderFields.fingerprint; typecheck | typecheck | `cd apps/api && npx tsc --noEmit -p tsconfig.json` | n/a | ⬜ pending |
|
||||||
| 13-02-01 | 02 | 1 | INGEST-07 | — | Adapter-Interface um portals[] generalisiert; DoeOpenDataAdapter deklariert portals | typecheck | `cd apps/api && npx tsc --noEmit -p tsconfig.json` | n/a | ⬜ pending |
|
| 13-02-01 | 02 | 1 | INGEST-07 | — | Adapter-Interface um portals[] generalisiert; DoeOpenDataAdapter deklariert portals | typecheck | `cd apps/api && npx tsc --noEmit -p tsconfig.json` | n/a | ⬜ pending |
|
||||||
| 13-02-02 | 02 | 1 | INGEST-07 | T-13-02-01/02 | SourceRegistry.register wirft DeniedPortalError fuer vergabe24 UND aumass (Erfolgskriterium 4); legitime Registrierung/get funktionieren | unit | `pnpm --filter @tessera/api test -- source-registry` | ❌ W0 | ⬜ pending |
|
| 13-02-02 | 02 | 1 | INGEST-07 | T-13-02-01/02 | SourceRegistry.register wirft DeniedPortalError fuer vergabe24 UND aumass (Erfolgskriterium 4); legitime Registrierung/get funktionieren | unit | `pnpm --filter @tessera/api test -- source-registry` | ❌ W0 | ⬜ pending |
|
||||||
| 13-03-01 | 03 | 2 | SCHEMA-03 | T-13-03-02/03/04 | 3-Stufen-Resolver OCID→source:noticeId→fingerprint; inert bei 1 Quelle (D-05), Merge->TenderSource anhaengen ab 2 (D-03); plain Prisma (kein RLS) | unit | `pnpm --filter @tessera/api test -- tender-dedup` | ❌ W0 | ⬜ pending |
|
| 13-03-01 | 03 | 2 | SCHEMA-03 | T-13-03-02/03/04 | 3-Stufen-Resolver OCID→source:noticeId→fingerprint; inert bei 1 Quelle (D-05), Merge->TenderSource anhaengen ab 2 (D-03); plain Prisma (kein RLS); SCHEMA-02-Erhalt (Match+geaenderter contentHash -> tender.update mutable Felder, kein Phase-10-Regress) | unit | `pnpm --filter @tessera/api test -- tender-dedup` | ❌ W0 | ⬜ pending |
|
||||||
| 13-03-02 | 03 | 2 | SCHEMA-03 | T-13-03-01/02 | pollDueSources fan-out (findMany isActive), catch-per-source, dedupActive=activePortalCount>=2, Delta-Boundary erhalten | unit | `pnpm --filter @tessera/api test -- tender-ingestion.service` | ⚠️ extend | ⬜ pending |
|
| 13-03-02 | 03 | 2 | SCHEMA-03 | T-13-03-01/02 | pollDueSources fan-out (findMany isActive), catch-per-source, dedupActive=activePortalCount>=2, Delta-Boundary erhalten | unit | `pnpm --filter @tessera/api test -- tender-ingestion.service` | ⚠️ extend | ⬜ pending |
|
||||||
| 13-03-03 | 03 | 2 | SCHEMA-03 | — | Modul-Wiring: SourceRegistry + TenderDedupService Provider + DoeOpenDataAdapter bei Boot registriert; DI aufloesbar | typecheck+suite | `cd apps/api && npx tsc --noEmit && pnpm --filter @tessera/api test -- src/tenders` | n/a | ⬜ pending |
|
| 13-03-03 | 03 | 2 | SCHEMA-03 | — | Modul-Wiring: SourceRegistry + TenderDedupService Provider + DoeOpenDataAdapter bei Boot registriert; DI aufloesbar | typecheck+suite | `cd apps/api && npx tsc --noEmit && pnpm --filter @tessera/api test -- src/tenders` | n/a | ⬜ pending |
|
||||||
| 13-06-01 | 06 | 2 | SCHEMA-03 | T-13-06-03 | getTender include sources[]; Route-Order unveraendert (:id zuletzt) | integration | `pnpm --filter @tessera/api test -- tenders.controller` | ⚠️ extend | ⬜ pending |
|
| 13-06-01 | 06 | 2 | SCHEMA-03 | T-13-06-03 | getTender include sources[]; Route-Order unveraendert (:id zuletzt) | integration | `pnpm --filter @tessera/api test -- tenders.controller` | ⚠️ extend | ⬜ pending |
|
||||||
@@ -67,7 +67,7 @@ Ein neues Package: `node-html-parser` (oder `cheerio`) — NUR nach blockierende
|
|||||||
|
|
||||||
- [ ] `apps/api/src/tenders/tender-fingerprint.spec.ts` — SCHEMA-03 NULL-Toleranz (Wert+Frist NULL -> gleicher Fingerprint), Kollisions-Fall (anderer Titel -> anderer Hash), Umlaut-/CPV-Division-Normalisierung, valueBucket, deterministischer sha256 (Plan 13-01 Task 2, RED-first).
|
- [ ] `apps/api/src/tenders/tender-fingerprint.spec.ts` — SCHEMA-03 NULL-Toleranz (Wert+Frist NULL -> gleicher Fingerprint), Kollisions-Fall (anderer Titel -> anderer Hash), Umlaut-/CPV-Division-Normalisierung, valueBucket, deterministischer sha256 (Plan 13-01 Task 2, RED-first).
|
||||||
- [ ] `apps/api/src/tenders/source-registry.spec.ts` — INGEST-07/Erfolgskriterium 4: register() wirft DeniedPortalError fuer vergabe24 UND aumass; gemischtes portals-Array abgelehnt; legitime Registrierung + get/activeAdapters (Plan 13-02 Task 2, RED-first).
|
- [ ] `apps/api/src/tenders/source-registry.spec.ts` — INGEST-07/Erfolgskriterium 4: register() wirft DeniedPortalError fuer vergabe24 UND aumass; gemischtes portals-Array abgelehnt; legitime Registrierung + get/activeAdapters (Plan 13-02 Task 2, RED-first).
|
||||||
- [ ] `apps/api/src/tenders/tender-dedup.service.spec.ts` — D-04/D-05: inert bei dedupActive=false (2 Tender), OCID-/noticeId-/Fingerprint-Match haengt TenderSource an (created=false), kein Match -> create (created=true); Prisma gemockt (Plan 13-03 Task 1, RED-first).
|
- [ ] `apps/api/src/tenders/tender-dedup.service.spec.ts` — D-04/D-05: inert bei dedupActive=false (2 Tender), OCID-/noticeId-/Fingerprint-Match haengt TenderSource an (created=false), kein Match -> create (created=true), SCHEMA-02-Erhalt (Match+geaenderter contentHash -> tender.update der mutable Felder); Prisma gemockt (Plan 13-03 Task 1, RED-first).
|
||||||
- [ ] `apps/api/src/tenders/adapters/netserver.adapter.spec.ts` + `__fixtures__/netserver-search.html` — INGEST-02: parst live-gecapturte <table>-Fixture -> RawTenderRecord[] mit korrektem sourcePortal je Zeile; []-Fallback bei kaputtem HTML; Fetch gemockt (Plan 13-04 Task 1).
|
- [ ] `apps/api/src/tenders/adapters/netserver.adapter.spec.ts` + `__fixtures__/netserver-search.html` — INGEST-02: parst live-gecapturte <table>-Fixture -> RawTenderRecord[] mit korrektem sourcePortal je Zeile; []-Fallback bei kaputtem HTML; Fetch gemockt (Plan 13-04 Task 1).
|
||||||
- [ ] `apps/api/src/tenders/adapters/cosinex.adapter.spec.ts` + `__fixtures__/cosinex-search.html` — INGEST-03: parst Satellite-Fixture ODER dokumentierter needs-JS-Leer-Fall; []-Fallback; Fetch gemockt (Plan 13-05 Task 1).
|
- [ ] `apps/api/src/tenders/adapters/cosinex.adapter.spec.ts` + `__fixtures__/cosinex-search.html` — INGEST-03: parst Satellite-Fixture ODER dokumentierter needs-JS-Leer-Fall; []-Fallback; Fetch gemockt (Plan 13-05 Task 1).
|
||||||
- [ ] (extend) `apps/api/src/tenders/tender-ingestion.service.spec.ts` — fan-out ueber mehrere aktive Configs, catch-per-source (Quelle A wirft -> B laeuft), dedupActive-Gate, matchDelta nur mit created-IDs (Plan 13-03 Task 2).
|
- [ ] (extend) `apps/api/src/tenders/tender-ingestion.service.spec.ts` — fan-out ueber mehrere aktive Configs, catch-per-source (Quelle A wirft -> B laeuft), dedupActive-Gate, matchDelta nur mit created-IDs (Plan 13-03 Task 2).
|
||||||
|
|||||||
Reference in New Issue
Block a user