---
phase: 13-scraping-adapters-cross-source-dedup
plan: 04
type: execute
wave: 3
depends_on: ["13-02", "13-03"]
files_modified:
- apps/api/package.json
- apps/api/src/tenders/adapters/netserver.adapter.ts
- apps/api/src/tenders/adapters/netserver.adapter.spec.ts
- apps/api/src/tenders/__fixtures__/netserver-search.html
- apps/api/src/tenders/tenders.module.ts
autonomous: false
requirements: [INGEST-02]
user_setup: []
must_haves:
truths:
- "EIN config-getriebener NetServer-Adapter bedient tender24, lhs-vpbw und vergabe.landbw (portals-Array, Base-URL je Portal)."
- "Der Adapter parst die NetServer-
-Trefferliste zu RawTenderRecord[] und setzt sourcePortal je Zeile korrekt."
- "Bei Parse-Totalausfall liefert der Adapter [] statt zu werfen (D-01, catch-per-source im Ingestion-Tick faengt den Rest)."
artifacts:
- apps/api/src/tenders/adapters/netserver.adapter.ts
- apps/api/src/tenders/__fixtures__/netserver-search.html
key_links:
- "SourceRegistry.register(netServerAdapter) bei Boot; pollDueSources fan-out ruft fetchTenders je aktiver Config."
---
Baut den NetServer-Adapter (INGEST-02 / Erfolgskriterium 1): EIN config-getriebener HTML-Adapter fuer die drei AI-AG-Vergabe@Net-Portale (tender24, lhs-vpbw, vergabe.landbw), der die oeffentliche `PublicationSearchControllerServlet`-Trefferliste (``, live verifiziert HTTP 200/161 KB, keine Auth) parst. Selektoren best-effort gefuellt (D-01); die Adapter-Grenze kapselt Fragilitaet — Test laeuft gegen eine live-gecapturte HTML-Fixture, unabhaengig von Live-Erreichbarkeit. Zuerst ein blockierender Package-Legitimacy-Checkpoint vor dem HTML-Parser-Install.
Purpose: Zweite reale Quelle neben DÖE — erst dadurch wird der Dedup-Kern (13-03) live wirksam (activePortalCount >= 2).
Output: node-html-parser (gated), netserver.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/doe-opendata.adapter.ts
@apps/api/src/tenders/adapters/tender-source-adapter.interface.ts
@apps/api/src/tenders/source-registry.ts
Task 1: Checkpoint — Package-Legitimacy-Gate HTML-Parser (vor Install)
Blockierender Package-Legitimacy-Checkpoint VOR jedem Install (13-RESEARCH.md § Package Legitimacy Audit). node-html-parser ist [ASSUMED] aus Trainingswissen, nicht autoritativ — Registry/Alter/Downloads/Repo pruefen, Wahl (node-html-parser oder cheerio) bestaetigen, erst dann installieren. Nie auto-approve.
Package-Legitimacy-Gate fuer den HTML-Parser VOR jedem Install (13-RESEARCH.md § Package Legitimacy Audit; node-html-parser ist [ASSUMED] aus Trainingswissen, nicht autoritativ).
1. Fuehre `gsd-tools query package-legitimacy check --ecosystem npm node-html-parser cheerio` aus (bzw. `npm view node-html-parser` + `npm view cheerio`): Alter, Wochendownloads, Repo pruefen (github.com/taoqf/node-html-parser).
2. Entscheide: `node-html-parser` (leichtgewichtig, CSS-Selektoren, empfohlen) ODER `cheerio` (vollere jQuery-API, weit verbreitet). Beide erfuellen den Zweck.
3. Bestaetige die Wahl; erst dann installiert der Executor `pnpm --filter @tessera/api add `.
Tippe "node-html-parser", "cheerio", oder beschreibe Bedenken.
Task 2: NetServer-Adapter (config-getrieben, 3 Portale) + HTML-Fixture + Spec
apps/api/src/tenders/adapters/netserver.adapter.ts, apps/api/src/tenders/adapters/netserver.adapter.spec.ts, apps/api/src/tenders/__fixtures__/netserver-search.html, apps/api/package.json
Installiere zuerst das im Checkpoint bestaetigte Package. Implementiere `netserver.adapter.ts` (`@Injectable`, implements TenderSourceAdapter, 13-RESEARCH.md Pattern 1): `sourceType = 'ai-netserver'`, `portals = ['tender24','lhs-vpbw','vergabe.landbw'] as const`, plus interne `NETSERVER_PORTALS`-Config-Map mit exakter Base-URL je Portal (tender24: `https://www.tender24.de`; lhs-vpbw: `https://lhs-vpbw.vmstart.de`; vergabe.landbw: `https://vergabe.landbw.de` — Base-URL MUSS direkt auf `/NetServer` zeigen, meta-refresh-Stub vermeiden, Pitfall 4). `fetchTenders(dayCursor)` iteriert die Portale, ruft je `${baseUrl}/NetServer/PublicationSearchControllerServlet?function=SearchPublications` via native `fetch` + `AbortController` 15s-Timeout (DoeOpenData-Muster, kein axios), `redirect:'follow'`, parst die ``-Trefferzeilen mit dem HTML-Parser zu `RawTenderRecord[]`, setzt `sourcePortal` je Zeile korrekt, extrahiert Zeilen-Anchor-href als `sourceUrl` und eine stabile ID daraus als `sourceNoticeId` (Open Question 2). Fehlertoleranz (Pitfall 2 / D-01): try/catch pro Zeile (`logger.warn`, Zeile ueberspringen), `[]` bei Totalausfall statt Throw; Pagination best-effort (erste Seite genuegt fuer MVP-Nachweis).
Speichere eine live-gecapturte NetServer-Trefferliste als `__fixtures__/netserver-search.html` (stabile Testgrundlage). Schreibe `netserver.adapter.spec.ts`: Adapter parst die Fixture -> N RawTenderRecord[] mit korrektem sourcePortal/sourceUrl/sourceNoticeId; leeres/kaputtes HTML -> []; Fetch wird gemockt (kein Live-Netz im Test). SSRF-Schutz (V5): Base-URLs sind hardcodierte Konstanten, NIE aus Input interpoliert (T-10-06).
pnpm --filter @tessera/api test -- netserver.adapter
Adapter parst die Fixture zu RawTenderRecord[] mit korrektem sourcePortal je Zeile; []-Fallback bei kaputtem HTML; Spec gruen. Falls ein Portal live nicht parsebar: als "needs-JS/blocked, deferred" im SUMMARY dokumentieren (D-01), Adapter-Skelett + Test bleiben gruen.
Task 3: NetServer-Adapter im Modul registrieren
apps/api/src/tenders/tenders.module.ts
Ergaenze `NetServerAdapter` als Provider in `tenders.module.ts` und haenge ihn additiv in die in 13-03 gebaute Boot-Registrierungsstelle (`SourceRegistry.register(netServerAdapter)`). Portale tender24/lhs-vpbw/vergabe.landbw sind NICHT auf der Denylist -> Registrierung erlaubt. Lege optional eine `TenderSourcePollConfig`-Row fuer `ai-netserver` (isActive default false, kein Zwang zur Aktivierung in P13 — Aktivierung ist Admin/Seed). DI-Graph muss aufloesbar bleiben; dieser Plan ist der einzige Wave-3-Schreiber von tenders.module.ts (13-05 folgt serialisiert in Wave 4).
cd apps/api && npx tsc --noEmit -p tsconfig.json && pnpm --filter @tessera/api test -- src/tenders
NetServerAdapter ist Provider + bei Boot registriert; DI aufloesbar; tenders-Suite gruen.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| NetServer-Portal (Internet) -> Adapter | Extern gefetchtes HTML ist untrusted (V5). |
| Package-Install -> Build | Neue Dependency node-html-parser/cheerio (Supply-Chain). |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-13-04-SC | Tampering | node-html-parser/cheerio Install | high | mitigate | Blocking-human Package-Legitimacy-Checkpoint VOR Install (nie auto-approve); [ASSUMED] bis Gate-Lauf. |
| T-13-04-01 | Tampering (SSRF) | Portal-Base-URL | high | mitigate | Hardcodierte Konstanten je Portal, NIE aus Input interpoliert (T-10-06). |
| T-13-04-02 | Denial of Service | malformed HTML / haengender Fetch | high | mitigate | AbortController-15s-Timeout, try/catch pro Zeile + []-Fallback, catch-per-source im Tick (D-01). |
| T-13-04-03 | Info Disclosure | Roh-HTML an Frontend | medium | mitigate | Parser-Ausgabe als Text coercen (Titel/Buyer), kein Roh-HTML weiterreichen (V5). |
- Checkpoint bestaetigt (Package-Wahl), erst dann Install.
- `pnpm --filter @tessera/api test -- netserver.adapter` gruen (gegen Fixture).
- `npx tsc --noEmit` + `pnpm --filter @tessera/api test -- src/tenders` gruen.
- Manuell (13-VALIDATION.md): optionaler Live-Poll gegen tender24 liefert >0 Records ODER dokumentierter "needs-JS/blocked"-Vermerk.
EIN config-getriebener Adapter bedient die 3 NetServer-Portale, parst die Trefferliste, ist fehlertolerant. Erfuellt INGEST-02 / Erfolgskriterium 1.