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,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>
|
||||
Reference in New Issue
Block a user