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:
@@ -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)."
|
||||
- "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)."
|
||||
- "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:
|
||||
- apps/api/src/tenders/tender-dedup.service.ts
|
||||
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, 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).
|
||||
- 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.
|
||||
- tenderSource.upsert nutzt @@unique([sourcePortal, sourceNoticeId]) als where-Target.
|
||||
</behavior>
|
||||
<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>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/api test -- tender-dedup</automated>
|
||||
</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 type="auto">
|
||||
@@ -79,7 +83,7 @@ WICHTIG (T-10-09 / Phase-10-Muster): plain PrismaService, KEIN forTenant()/RLS
|
||||
<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')`.
|
||||
|
||||
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>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/api test -- tender-ingestion.service</automated>
|
||||
|
||||
Reference in New Issue
Block a user