---
phase: 13-scraping-adapters-cross-source-dedup
plan: 05
type: execute
wave: 4
depends_on: ["13-04"]
files_modified:
- 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
- apps/api/src/tenders/tenders.module.ts
autonomous: true
requirements: [INGEST-03]
must_haves:
truths:
- "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."
artifacts:
- apps/api/src/tenders/adapters/cosinex.adapter.ts
- apps/api/src/tenders/__fixtures__/cosinex-search.html
key_links:
- "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.
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
@.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.
## 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). |
- `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.
Separater cosinex/DTVP-Adapter parst die Trefferliste (best-effort, D-01) und ist registriert. Erfuellt INGEST-03 / Erfolgskriterium 2.