6 plans (INGEST-02/03/07, SCHEMA-03) + Nyquist validation.
Core (registry, fingerprint, dedup, TenderSource, backfill) lands
first and is green independent of live scraping (D-01).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ein separater cosinex/DTVP-Adapter (getrennt vom NetServer-Adapter, cosinex-HTML != NetServer) parst die oeffentliche Satellite-Trefferliste zu RawTenderRecord[].
Bei JS-abhaengiger/nicht-parsebarer Trefferliste liefert der Adapter [] und der Fall ist als 'needs-JS, deferred' dokumentiert (D-01) — Adapter-Grenze + Test bleiben gruen.
sourceType = 'cosinex-dtvp', sourcePortal je Record korrekt gesetzt.
SourceRegistry.register(cosinexAdapter) bei Boot; getrennter Adapter, kein NetServer-Reuse.
Baut den cosinex/DTVP-Adapter (INGEST-03 / Erfolgskriterium 2): ein SEPARATER HTML-Adapter fuer den cosinex-Vergabemarktplatz (`www.dtvp.de/Satellite/company/welcome.do`, live HTTP 200/40 KB, oeffentlich) — cosinex-HTML unterscheidet sich strukturell vom NetServer, daher kein geteilter Adapter. Selektoren best-effort (D-01): falls die Trefferliste server-seitig gerendert ist, werden die Selektoren gefuellt; ist sie JS-abhaengig, wird der Adapter als "needs-JS, deferred" dokumentiert, liefert `[]`, und Adapter-Skelett + Test bleiben trotzdem gruen (Open Question 1, kein Headless-Browser im Stack).
Purpose: Dritte Quelle; erfuellt INGEST-03 und liefert einen weiteren realen Merge-Kandidaten fuer den Dedup-Kern.
Output: cosinex.adapter.ts (+ spec + fixture), Modul-Registrierung.
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-scraping-adapters-cross-source-dedup/13-CONTEXT.md
@.planning/phases/13-scraping-adapters-cross-source-dedup/13-RESEARCH.md
@apps/api/src/tenders/adapters/netserver.adapter.ts
@apps/api/src/tenders/adapters/tender-source-adapter.interface.ts
@apps/api/src/tenders/source-registry.ts
Task 1: cosinex/DTVP-Adapter + HTML-Fixture + Spec
apps/api/src/tenders/adapters/cosinex.adapter.ts, apps/api/src/tenders/adapters/cosinex.adapter.spec.ts, apps/api/src/tenders/__fixtures__/cosinex-search.html
Implementiere `cosinex.adapter.ts` (`@Injectable`, implements TenderSourceAdapter) als EIGENEN Adapter (nicht NetServer wiederverwenden): `sourceType = 'cosinex-dtvp'`, `portals = ['cosinex-dtvp'] as const`, Base-URL `https://www.dtvp.de`. `fetchTenders(dayCursor)` fetcht die oeffentliche Satellite-Trefferliste (`Satellite/company/welcome.do` bzw. die konkrete Suchseite) via native `fetch` + AbortController 15s (DoeOpenData/NetServer-Muster, kein axios), parst mit dem in 13-04 bereits installierten HTML-Parser die server-gerenderten Trefferzeilen zu `RawTenderRecord[]`, setzt sourcePortal='cosinex-dtvp', extrahiert sourceUrl + stabile sourceNoticeId je Zeile. Fehlertoleranz identisch zu NetServer (Pitfall 2 / D-01): try/catch pro Zeile, `[]` bei Totalausfall, kein Throw.
Speichere eine live-gecapturte cosinex-Trefferliste als __fixtures__/cosinex-search.html. Schreibe cosinex.adapter.spec.ts: Adapter parst die Fixture -> RawTenderRecord[] mit sourceType/sourcePortal/sourceUrl/sourceNoticeId; kaputtes/leeres HTML -> []; Fetch gemockt. Falls die Live-Trefferliste JS-abhaengig ist (Open Question 1): Test gegen den server-gerenderten Teil oder gegen einen dokumentierten Leer-Fall; im SUMMARY als "needs-JS, deferred" vermerken. SSRF (V5): Base-URL hardcodiert.
pnpm --filter @tessera/api test -- cosinex.adapter
cosinex-Adapter parst die Fixture (oder liefert dokumentiert [] bei JS-Abhaengigkeit); []-Fallback bei kaputtem HTML; Spec gruen; getrennt vom NetServer-Adapter.
Task 2: cosinex-Adapter im Modul registrieren
apps/api/src/tenders/tenders.module.ts
Ergaenze `CosinexAdapter` als Provider in `tenders.module.ts` und registriere ihn additiv in der Boot-Registrierungsstelle (`SourceRegistry.register(cosinexAdapter)`). Portal cosinex-dtvp ist nicht auf der Denylist. Optional `TenderSourcePollConfig`-Row fuer `cosinex-dtvp` (isActive default false). Dieser Plan ist der einzige Wave-4-Schreiber von tenders.module.ts (serialisiert nach 13-04). DI-Graph aufloesbar.
cd apps/api && npx tsc --noEmit -p tsconfig.json && pnpm --filter @tessera/api test -- src/tenders
CosinexAdapter ist Provider + bei Boot registriert; DI aufloesbar; tenders-Suite gruen.
<threat_model>
Trust Boundaries
Boundary
Description
cosinex/DTVP-Portal (Internet) -> Adapter
Extern gefetchtes HTML ist untrusted (V5).
STRIDE Threat Register
Threat ID
Category
Component
Severity
Disposition
Mitigation Plan
T-13-05-01
Tampering (SSRF)
cosinex Base-URL
high
mitigate
Hardcodierte Konstante, nie aus Input interpoliert (T-10-06).
T-13-05-02
Denial of Service
JS-lastiges/malformed HTML, haengender Fetch
high
mitigate
AbortController-15s, try/catch pro Zeile, []-Fallback, catch-per-source (D-01).
T-13-05-03
Info Disclosure
Roh-HTML an Frontend
medium
mitigate
Parser-Ausgabe als Text coercen (V5).
</threat_model>
- `pnpm --filter @tessera/api test -- cosinex.adapter` gruen (Fixture oder dokumentierter Leer-Fall).
- `npx tsc --noEmit` + `pnpm --filter @tessera/api test -- src/tenders` gruen.
- Manuell (13-VALIDATION.md): optionaler Live-Fetch dtvp welcome.do liefert parsebares HTML ODER "needs-JS"-Vermerk dokumentiert.
<success_criteria>
Separater cosinex/DTVP-Adapter parst die Trefferliste (best-effort, D-01) und ist registriert. Erfuellt INGEST-03 / Erfolgskriterium 2.
</success_criteria>
Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-05-SUMMARY.md` when done