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>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
---
|
||||
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."
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: cosinex/DTVP-Adapter + HTML-Fixture + Spec</name>
|
||||
<files>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</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pnpm --filter @tessera/api test -- cosinex.adapter</automated>
|
||||
</verify>
|
||||
<done>cosinex-Adapter parst die Fixture (oder liefert dokumentiert [] bei JS-Abhaengigkeit); []-Fallback bei kaputtem HTML; Spec gruen; getrennt vom NetServer-Adapter.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: cosinex-Adapter im Modul registrieren</name>
|
||||
<files>apps/api/src/tenders/tenders.module.ts</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && npx tsc --noEmit -p tsconfig.json && pnpm --filter @tessera/api test -- src/tenders</automated>
|
||||
</verify>
|
||||
<done>CosinexAdapter ist Provider + bei Boot registriert; DI aufloesbar; tenders-Suite gruen.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
- `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.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
Separater cosinex/DTVP-Adapter parst die Trefferliste (best-effort, D-01) und ist registriert. Erfuellt INGEST-03 / Erfolgskriterium 2.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-05-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user