Files
tessera-ctl/.planning/phases/13-scraping-adapters-cross-source-dedup/13-04-PLAN.md
T
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

8.8 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
13-scraping-adapters-cross-source-dedup 04 execute 3
13-02
13-03
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
false
INGEST-02
truths artifacts key_links
EIN config-getriebener NetServer-Adapter bedient tender24, lhs-vpbw und vergabe.landbw (portals-Array, Base-URL je Portal).
Der Adapter parst die NetServer-<table>-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).
apps/api/src/tenders/adapters/netserver.adapter.ts
apps/api/src/tenders/__fixtures__/netserver-search.html
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.

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

<threat_model>

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).
</threat_model>
- 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.

<success_criteria> EIN config-getriebener Adapter bedient die 3 NetServer-Portale, parst die Trefferliste, ist fehlertolerant. Erfuellt INGEST-02 / Erfolgskriterium 1. </success_criteria>

Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-04-SUMMARY.md` when done