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

6.3 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 02 execute 1
apps/api/src/tenders/source-registry.ts
apps/api/src/tenders/source-registry.spec.ts
apps/api/src/tenders/adapters/tender-source-adapter.interface.ts
true
INGEST-07
truths artifacts key_links
Der Versuch, einen Adapter mit portal 'vergabe24' oder 'aumass' zu registrieren, wirft eine DeniedPortalError-Exception im Code (nicht nur Doku).
Ein Test beweist die Ablehnung (Erfolgskriterium 4).
Ein Adapter deklariert seine bedienten Portale ueber ein readonly portals-Array (NetServer: 3).
apps/api/src/tenders/source-registry.ts
apps/api/src/tenders/adapters/tender-source-adapter.interface.ts
SourceRegistry.register() -> assertNotDenied ueber alle adapter.portals bei DI-Boot.
Baut das INGEST-07-Rueckgrat: eine `SourceRegistry` mit hartem Denylist-Gate, die vergabe24/aumass strukturell im Code ablehnt (D-06 / Erfolgskriterium 4), plus die Generalisierung des Adapter-Vertrags (`portals`-Array), damit EIN NetServer-Adapter mehrere Portale bedienen kann. Voll getestet, ohne jede Scraping-Abhaengigkeit (D-01).

Purpose: Compliance-/Sicherheitsgrenze gehoert testbar in den DI-Layer, bevor irgendein Poll laeuft. Output: source-registry.ts (Registry + DeniedPortalError), source-registry.spec.ts (Refusal-Beweis), generalisiertes TenderSourceAdapter-Interface.

<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/tender-source-adapter.interface.ts @apps/api/src/tenders/adapters/doe-opendata.adapter.ts Task 1: Adapter-Interface generalisieren (portals-Array) apps/api/src/tenders/adapters/tender-source-adapter.interface.ts Erweitere `TenderSourceAdapter` (13-RESEARCH.md Pattern 1) um `readonly portals: readonly string[]` — die Liste der von diesem Adapter bedienten Quell-Portale (DÖE: `['doe-opendata']`; NetServer spaeter: `['tender24','lhs-vpbw','vergabe.landbw']`). `sourceType` und `fetchTenders(dayCursor)` bleiben unveraendert. Aktualisiere den bestehenden `DoeOpenDataAdapter`, sodass er `readonly portals = ['doe-opendata'] as const` deklariert (minimaler additiver Edit, keine Verhaltensaenderung). Kommentar: `portals` entkoppelt "ein Adapter" von "ein Portal" (config-driven multi-portal, INGEST-02). cd apps/api && npx tsc --noEmit -p tsconfig.json Interface hat `portals`; DoeOpenDataAdapter deklariert es; `tsc --noEmit` gruen; bestehende doe-Adapter-Spec bleibt gruen. Task 2: SourceRegistry + Denylist-Gate + Refusal-Test (INGEST-07 / D-06) apps/api/src/tenders/source-registry.ts, apps/api/src/tenders/source-registry.spec.ts - register() eines Fake-Adapters mit portals ['vergabe24'] wirft DeniedPortalError. - register() mit portals ['aumass'] wirft DeniedPortalError. - register() eines legitimen Adapters (portals ['tender24']) fuegt ihn hinzu; get(sourceType) liefert ihn zurueck. - activeAdapters() liefert alle registrierten Adapter. - get() eines nicht registrierten sourceType liefert undefined (kein Throw). - Ein gemischter Adapter (portals ['tender24','aumass']) wird komplett abgelehnt (ein verbotenes Portal genuegt). Implementiere `source-registry.ts` nach 13-RESEARCH.md Pattern 2: exportiere `DENYLISTED_PORTALS = ['vergabe24','aumass'] as const`, Klasse `DeniedPortalError extends Error` (deutsche Meldung: AGB-seitig gesperrt), und `@Injectable() class SourceRegistry` mit `private adapters = new Map()`, `register(adapter)` (iteriert `adapter.portals`, wirft DeniedPortalError bei Treffer in DENYLISTED_PORTALS, sonst set), `get(type)` und `activeAdapters()`. Der Denylist-Vergleich ist der Sicherheitskern (D-06) — harte Code-Exception, NIE nur Doku. Schreibe die Spec RED-first exakt nach behavior-Block; sie ist der Beweis fuer Erfolgskriterium 4. Verwende einen Fake-Adapter-Stub im Test (kein echtes Scraping). Beachte die comment-text-Disziplin: kein negativer grep-Gate auf 'vergabe24'/'aumass' im Datei-Body ausserhalb der Konstante. pnpm --filter @tessera/api test -- source-registry register() wirft DeniedPortalError fuer vergabe24 UND aumass (Test gruen); legitime Registrierung + get()/activeAdapters() funktionieren; kein Prisma/Scraping-Import.

<threat_model>

Trust Boundaries

Boundary Description
Adapter-Registrierung -> Poll-System Ein verbotenes Portal darf nie in den aktiven Poll-Pfad gelangen (AGB/Compliance).

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-13-02-01 Elevation of Privilege Registrierung eines Denylist-Portals high mitigate Harte DeniedPortalError-Exception in register() bei DI-Boot; Test beweist Refusal (D-06, INGEST-07).
T-13-02-02 Tampering Umgehung des Gates via gemischtem portals-Array high mitigate Iteration ueber ALLE portals; ein verbotenes Portal lehnt den ganzen Adapter ab.
T-13-02-SC Tampering keine neue Dependency in diesem Plan low accept Kern nutzt nur bestehende Deps; node-html-parser erst in 13-04 hinter Checkpoint.
</threat_model>
- `pnpm --filter @tessera/api test -- source-registry` gruen (Refusal beweist Erfolgskriterium 4). - `npx tsc --noEmit` gruen.

<success_criteria> SourceRegistry lehnt vergabe24/aumass im Code ab (getestet); Adapter-Vertrag um portals generalisiert. Erfuellt INGEST-07 strukturell. </success_criteria>

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