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:
2026-07-23 08:24:16 +02:00
parent f2e0fc8cef
commit 74c00166e2
8 changed files with 827 additions and 1 deletions
@@ -0,0 +1,110 @@
---
phase: 13-scraping-adapters-cross-source-dedup
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- 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
autonomous: true
requirements: [INGEST-07]
must_haves:
truths:
- "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)."
artifacts:
- apps/api/src/tenders/source-registry.ts
- apps/api/src/tenders/adapters/tender-source-adapter.interface.ts
key_links:
- "SourceRegistry.register() -> assertNotDenied ueber alle adapter.portals bei DI-Boot."
---
<objective>
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.
</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/tender-source-adapter.interface.ts
@apps/api/src/tenders/adapters/doe-opendata.adapter.ts
</context>
<tasks>
<task type="auto">
<name>Task 1: Adapter-Interface generalisieren (portals-Array)</name>
<files>apps/api/src/tenders/adapters/tender-source-adapter.interface.ts</files>
<action>
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).
</action>
<verify>
<automated>cd apps/api && npx tsc --noEmit -p tsconfig.json</automated>
</verify>
<done>Interface hat `portals`; DoeOpenDataAdapter deklariert es; `tsc --noEmit` gruen; bestehende doe-Adapter-Spec bleibt gruen.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: SourceRegistry + Denylist-Gate + Refusal-Test (INGEST-07 / D-06)</name>
<files>apps/api/src/tenders/source-registry.ts, apps/api/src/tenders/source-registry.spec.ts</files>
<behavior>
- 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).
</behavior>
<action>
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<SourceType, TenderSourceAdapter>()`, `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.
</action>
<verify>
<automated>pnpm --filter @tessera/api test -- source-registry</automated>
</verify>
<done>register() wirft DeniedPortalError fuer vergabe24 UND aumass (Test gruen); legitime Registrierung + get()/activeAdapters() funktionieren; kein Prisma/Scraping-Import.</done>
</task>
</tasks>
<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>
<verification>
- `pnpm --filter @tessera/api test -- source-registry` gruen (Refusal beweist Erfolgskriterium 4).
- `npx tsc --noEmit` gruen.
</verification>
<success_criteria>
SourceRegistry lehnt vergabe24/aumass im Code ab (getestet); Adapter-Vertrag um portals generalisiert. Erfuellt INGEST-07 strukturell.
</success_criteria>
<output>
Create `.planning/phases/13-scraping-adapters-cross-source-dedup/13-02-SUMMARY.md` when done
</output>