--- 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. Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-05-SUMMARY.md` when done