Files
schalli 74c00166e2 docs(13): create phase plan — scraping adapters + cross-source dedup
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>
2026-07-23 08:24:16 +02:00

6.4 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
13-scraping-adapters-cross-source-dedup 05 execute 4
13-04
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
true
INGEST-03
truths artifacts key_links
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.
apps/api/src/tenders/adapters/cosinex.adapter.ts
apps/api/src/tenders/__fixtures__/cosinex-search.html
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.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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