docs(13-02): complete source-registry-denylist plan

This commit is contained in:
2026-07-23 08:44:53 +02:00
parent 0fa9567571
commit 83d8eff40d
4 changed files with 151 additions and 12 deletions
+2 -2
View File
@@ -16,7 +16,7 @@
- [ ] **INGEST-04**: Das System importiert Ausschreibungen aus RSS-Feeds (subreport-elvis, service.bund.de). - [ ] **INGEST-04**: Das System importiert Ausschreibungen aus RSS-Feeds (subreport-elvis, service.bund.de).
- [ ] **INGEST-05**: Das System liest Portal-Benachrichtigungs-E-Mails aus einem konfigurierten Postfach ein (nutzt bestehende DKV-Inbox-Infrastruktur) und extrahiert daraus Ausschreibungen. - [ ] **INGEST-05**: Das System liest Portal-Benachrichtigungs-E-Mails aus einem konfigurierten Postfach ein (nutzt bestehende DKV-Inbox-Infrastruktur) und extrahiert daraus Ausschreibungen.
- [x] **INGEST-06**: Jede Quelle wird einmal zentral pro Zeitplan abgefragt (poll-once-fan-out-many), Intervall pro Quelle im Admin-Bereich konfigurierbar; das Ergebnis wird an alle passenden Mandanten-Suchprofile verteilt. - [x] **INGEST-06**: Jede Quelle wird einmal zentral pro Zeitplan abgefragt (poll-once-fan-out-many), Intervall pro Quelle im Admin-Bereich konfigurierbar; das Ergebnis wird an alle passenden Mandanten-Suchprofile verteilt.
- [ ] **INGEST-07**: vergabe24 und aumass sind als harte Denylist hinterlegt und können nicht als automatische Scraping-Quelle registriert werden (AGB-Verbot). - [x] **INGEST-07**: vergabe24 und aumass sind als harte Denylist hinterlegt und können nicht als automatische Scraping-Quelle registriert werden (AGB-Verbot).
### SCHEMA — Normalisierung & Deduplizierung ### SCHEMA — Normalisierung & Deduplizierung
@@ -98,7 +98,7 @@
| NOTIFY-04 | Phase 12 | Complete | | NOTIFY-04 | Phase 12 | Complete |
| INGEST-02 | Phase 13 | Pending | | INGEST-02 | Phase 13 | Pending |
| INGEST-03 | Phase 13 | Pending | | INGEST-03 | Phase 13 | Pending |
| INGEST-07 | Phase 13 | Pending | | INGEST-07 | Phase 13 | Complete |
| SCHEMA-03 | Phase 13 | Complete | | SCHEMA-03 | Phase 13 | Complete |
| INGEST-04 | Phase 14 | Pending | | INGEST-04 | Phase 14 | Pending |
| INGEST-05 | Phase 14 | Pending | | INGEST-05 | Phase 14 | Pending |
+3 -3
View File
@@ -439,12 +439,12 @@ Plans:
3. A tender that appears via both DÖE and a scraping adapter shows up once in the results list (fuzzy fingerprint dedup on buyer+title+CPV+deadline+value), with links to all of its source portals -- dedup logic only activates once a second source is live 3. A tender that appears via both DÖE and a scraping adapter shows up once in the results list (fuzzy fingerprint dedup on buyer+title+CPV+deadline+value), with links to all of its source portals -- dedup logic only activates once a second source is live
4. Attempting to register vergabe24 or aumass as a poll source is refused by the system itself (adapter registry denylist enforced in code), not just documented as forbidden 4. Attempting to register vergabe24 or aumass as a poll source is refused by the system itself (adapter registry denylist enforced in code), not just documented as forbidden
**Plans**: 1/6 plans executed **Plans**: 2/6 plans executed
Plans: Plans:
- [x] 13-01-PLAN.md — TenderSource-Schema + Backfill + NULL-tolerante Fingerprint-Fn + SourceType-Union (SCHEMA-03 Datenschicht) - [x] 13-01-PLAN.md — TenderSource-Schema + Backfill + NULL-tolerante Fingerprint-Fn + SourceType-Union (SCHEMA-03 Datenschicht)
- [ ] 13-02-PLAN.md — SourceRegistry + harte Denylist-Gate (vergabe24/aumass) + Adapter-Interface-Generalisierung (INGEST-07) - [x] 13-02-PLAN.md — SourceRegistry + harte Denylist-Gate (vergabe24/aumass) + Adapter-Interface-Generalisierung (INGEST-07)
- [ ] 13-03-PLAN.md — 3-Stufen-Dedup-Resolver + pollDueSources Fan-out (catch-per-source) + Modul-Wiring (SCHEMA-03) - [ ] 13-03-PLAN.md — 3-Stufen-Dedup-Resolver + pollDueSources Fan-out (catch-per-source) + Modul-Wiring (SCHEMA-03)
- [ ] 13-04-PLAN.md — NetServer-Adapter (config-getrieben, 3 Portale) + Package-Legitimacy-Checkpoint (INGEST-02) - [ ] 13-04-PLAN.md — NetServer-Adapter (config-getrieben, 3 Portale) + Package-Legitimacy-Checkpoint (INGEST-02)
- [ ] 13-05-PLAN.md — cosinex/DTVP-Adapter (separat, best-effort) (INGEST-03) - [ ] 13-05-PLAN.md — cosinex/DTVP-Adapter (separat, best-effort) (INGEST-03)
@@ -486,5 +486,5 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
| 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 | | 10. Ausschreibungs-Radar Foundation & DÖE Ingestion | 6/6 | Complete | 2026-07-21 |
| 11. Filter Engine, Results UI & Saved Searches | 6/6 | In Progress| | | 11. Filter Engine, Results UI & Saved Searches | 6/6 | In Progress| |
| 12. Tender Notifications | 4/4 | In Progress| | | 12. Tender Notifications | 4/4 | In Progress| |
| 13. Scraping Adapters & Cross-Source Deduplication | 1/6 | In Progress| | | 13. Scraping Adapters & Cross-Source Deduplication | 2/6 | In Progress| |
| 14. RSS, Email-Alert Ingestion & Module Rollout | 0/TBD | Not started | - | | 14. RSS, Email-Alert Ingestion & Module Rollout | 0/TBD | Not started | - |
+9 -7
View File
@@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar
current_phase: 13 current_phase: 13
current_phase_name: scraping-adapters-cross-source-dedup current_phase_name: scraping-adapters-cross-source-dedup
status: executing status: executing
stopped_at: Completed 13-01-PLAN.md stopped_at: Completed 13-02-PLAN.md
last_updated: "2026-07-23T06:40:39.176Z" last_updated: "2026-07-23T06:44:34.483Z"
last_activity: 2026-07-23 last_activity: 2026-07-23
last_activity_desc: Phase 13 execution started last_activity_desc: Phase 13 execution started
progress: progress:
total_phases: 13 total_phases: 13
completed_phases: 11 completed_phases: 11
total_plans: 62 total_plans: 62
completed_plans: 56 completed_plans: 57
--- ---
# Project State # Project State
@@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17)
## Current Position ## Current Position
Phase: 13 (scraping-adapters-cross-source-dedup) — EXECUTING Phase: 13 (scraping-adapters-cross-source-dedup) — EXECUTING
Plan: 2 of 6 Plan: 3 of 6
Status: Ready to execute Status: Ready to execute
Last activity: 2026-07-23 — Phase 13 execution started Last activity: 2026-07-23 — Phase 13 execution started
Progress: [█████████░] 90% Progress: [█████████░] 92%
## Performance Metrics ## Performance Metrics
@@ -91,6 +91,7 @@ Progress: [█████████░] 90%
| Phase 12 P03 | 15min | 2 tasks | 3 files | | Phase 12 P03 | 15min | 2 tasks | 3 files |
| Phase 12 P04 | 12min | 3 tasks | 10 files | | Phase 12 P04 | 12min | 3 tasks | 10 files |
| Phase 13 P01 | 35min | 3 tasks | 6 files | | Phase 13 P01 | 35min | 3 tasks | 6 files |
| Phase 13 P02 | 20min | 2 tasks | 4 files |
## Accumulated Context ## Accumulated Context
@@ -194,6 +195,7 @@ Recent decisions affecting current work:
- [Phase ?]: Digest-interval selector inline in settings/page.tsx (already 'use client'); instantAlert toggle uses plain checkbox for chip-based SavedSearchBar UI - [Phase ?]: Digest-interval selector inline in settings/page.tsx (already 'use client'); instantAlert toggle uses plain checkbox for chip-based SavedSearchBar UI
- [Phase ?]: SCHEMA-03 fingerprint: title+buyer dominant, CPV division, value-bucket, deadline-day, sha256; dedupKey untouched, fingerprint additive - [Phase ?]: SCHEMA-03 fingerprint: title+buyer dominant, CPV division, value-bucket, deadline-day, sha256; dedupKey untouched, fingerprint additive
- [Phase ?]: One-time TS backfill scripts run via compiled dist/ output (not raw .ts execution) to keep tsc --noEmit clean - [Phase ?]: One-time TS backfill scripts run via compiled dist/ output (not raw .ts execution) to keep tsc --noEmit clean
- [Phase ?]: SourceRegistry.register() throws DeniedPortalError for any portal in DENYLISTED_PORTALS (vergabe24, aumass), enforced at DI-registration time not just documented (INGEST-07)
### Pending Todos ### Pending Todos
@@ -231,7 +233,7 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity ## Session Continuity
Last session: 2026-07-23T06:40:39.162Z Last session: 2026-07-23T06:44:34.470Z
Stopped at: Completed 13-01-PLAN.md Stopped at: Completed 13-02-PLAN.md
Resume file: None Resume file: None
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
@@ -0,0 +1,137 @@
---
phase: 13-scraping-adapters-cross-source-dedup
plan: 02
subsystem: backend
tags: [nestjs, di, security-gate, adapter-pattern]
requires:
- phase: 13-scraping-adapters-cross-source-dedup
plan: 01
provides: SourceType open union, TenderSourceAdapter interface (Phase 10 foundation)
provides:
- "TenderSourceAdapter.portals: readonly string[] — adapters declare all portals they serve"
- "SourceRegistry (Injectable) mapping SourceType -> TenderSourceAdapter, with get()/activeAdapters()"
- "DeniedPortalError + DENYLISTED_PORTALS = ['vergabe24', 'aumass'] — hard code-level registration refusal"
affects: [13-03-dedup-resolver-fan-out, 13-04-netserver-adapter, 13-05-cosinex-adapter]
tech-stack:
added: []
patterns:
- "Denylist enforced as a code exception at DI-registration time (SourceRegistry.register), not documentation-only — checked per-portal, so a mixed portals array is rejected wholesale if any single entry is denylisted"
key-files:
created:
- apps/api/src/tenders/source-registry.ts
- apps/api/src/tenders/source-registry.spec.ts
modified:
- apps/api/src/tenders/adapters/tender-source-adapter.interface.ts
- apps/api/src/tenders/adapters/doe-opendata.adapter.ts
key-decisions:
- "portals is a readonly string[] on the adapter interface (not a single sourcePortal) exactly per 13-RESEARCH Pattern 1/2 — lets one NetServer adapter (Plan 13-04) serve tender24/lhs-vpbw/vergabe.landbw while the registry still gates each portal individually"
- "Denylist check iterates ALL adapter.portals before registering any — one denylisted portal in a mixed array rejects the entire adapter, no partial registration (T-13-02-02)"
- "Fake adapter stub used in the spec (no real scraping/HTTP dependency) — keeps the registry test suite dependency-free per D-01"
patterns-established:
- "SourceRegistry.get()/activeAdapters() is the seam Plan 13-03's pollDueSources fan-out will use instead of a hardwired single-adapter injection"
requirements-completed: [INGEST-07]
coverage:
- id: D1
description: "Registering an adapter whose portals include 'vergabe24' or 'aumass' throws DeniedPortalError as a code-level exception (not documentation-only), including when the denylisted portal is mixed into an otherwise-legitimate portals array"
requirement: INGEST-07
verification:
- kind: unit
ref: "apps/api/src/tenders/source-registry.spec.ts — 'throws DeniedPortalError when registering an adapter serving vergabe24', 'throws DeniedPortalError when registering an adapter serving aumass', 'rejects a mixed portals array wholesale when one entry is denylisted' (6/6 tests pass)"
status: pass
human_judgment: false
- id: D2
description: "Legitimate adapters register and are retrievable via get(sourceType)/activeAdapters(); get() of an unregistered sourceType returns undefined without throwing"
requirement: INGEST-07
verification:
- kind: unit
ref: "apps/api/src/tenders/source-registry.spec.ts — 'registers a legitimate adapter...', 'activeAdapters() returns all registered adapters', 'get() returns undefined for an unregistered sourceType (no throw)'"
status: pass
human_judgment: false
- id: D3
description: "TenderSourceAdapter interface generalized with portals[]; DoeOpenDataAdapter declares portals = ['doe-opendata'] without behavior change; project-wide typecheck and full existing tenders test slice remain green"
requirement: INGEST-07
verification:
- kind: unit
ref: "cd apps/api && npx tsc --noEmit -p tsconfig.json (clean)"
status: pass
- kind: unit
ref: "cd apps/api && npx vitest run src/tenders (18 files, 177/177 tests pass, includes unchanged doe-opendata.adapter.spec.ts 6/6)"
status: pass
human_judgment: false
duration: 20min
completed: 2026-07-23
status: complete
---
# Phase 13 Plan 02: SourceRegistry + Denylist Gate Summary
**Injectable `SourceRegistry` that throws `DeniedPortalError` in code — not just documentation — the instant an adapter declares `vergabe24` or `aumass` among its `portals`, plus the generalized `TenderSourceAdapter.portals[]` contract that lets one adapter serve multiple portals.**
## Performance
- **Duration:** 20 min
- **Started:** 2026-07-23T08:41:00Z
- **Completed:** 2026-07-23T08:44:30Z
- **Tasks:** 2
- **Files modified:** 4 (2 created, 2 modified)
## Accomplishments
- `TenderSourceAdapter` interface gained `readonly portals: readonly string[]`; `DoeOpenDataAdapter` declares `portals = ['doe-opendata'] as const` — additive, no behavior change, existing spec (6/6) still green.
- New `SourceRegistry` (`@Injectable`): `register()` iterates every `adapter.portals` entry and throws `DeniedPortalError` (German AGB message) if any match `DENYLISTED_PORTALS = ['vergabe24', 'aumass']`; `get(sourceType)` returns `undefined` (not a throw) for unregistered types; `activeAdapters()` returns all registered adapters for Plan 13-03's fan-out scheduler.
- **Denylist refusal proof (Erfolgskriterium 4):** `source-registry.spec.ts` — RED-first TDD — proves `register()` throws for `portals: ['vergabe24']`, throws for `portals: ['aumass']`, and rejects a **mixed** array `['tender24', 'aumass']` wholesale (one denylisted portal is enough — no partial registration, T-13-02-02). Legitimate registration + `get()`/`activeAdapters()` also covered. 6/6 tests pass.
- `npx tsc --noEmit` clean and full `src/tenders` slice (18 files, 177/177 tests) green — no Prisma/scraping import introduced.
## Task Commits
Each task was committed atomically:
1. **Task 1: Adapter interface generalized with portals[]** - `1b11ada` (feat)
2. **Task 2a: RED — failing SourceRegistry denylist-gate spec** - `78b17ef` (test)
3. **Task 2b: GREEN — SourceRegistry implementation** - `0fa9567` (feat)
_TDD task (Task 2) produced two commits (test → feat) per protocol; no refactor commit was needed — the first implementation passed all 6 tests cleanly._
## Files Created/Modified
- `apps/api/src/tenders/adapters/tender-source-adapter.interface.ts` - added `portals: readonly string[]` field + doc explaining the denylist-gate hook
- `apps/api/src/tenders/adapters/doe-opendata.adapter.ts` - declares `readonly portals = ['doe-opendata'] as const`
- `apps/api/src/tenders/source-registry.ts` - `DENYLISTED_PORTALS`, `DeniedPortalError`, `@Injectable() SourceRegistry` (register/get/activeAdapters)
- `apps/api/src/tenders/source-registry.spec.ts` - 6 unit tests (denylist refusal x2, mixed-array refusal, legitimate register/get, activeAdapters, unregistered get)
## Decisions Made
- **`portals` as `readonly string[]` on the interface, exactly per 13-RESEARCH Pattern 1/2** — decouples "one adapter" from "one portal" so the future NetServer adapter (Plan 13-04) can serve `tender24`/`lhs-vpbw`/`vergabe.landbw` from one class while the registry still checks each portal individually.
- **Denylist check iterates the full `portals` array before any mutation of the adapters Map** — a mixed portals array containing even one denylisted entry is rejected entirely; the registry never partially registers an adapter.
- **Fake adapter stub in the spec** (no `doe-opendata.adapter.ts` reuse, no HTTP/Prisma) — keeps `source-registry.spec.ts` fully dependency-free, consistent with D-01 ("robust core first, independent of live scrapability").
## Deviations from Plan
None - plan executed exactly as written. No Rule 1-4 auto-fixes were needed; the plan's design (Pattern 1 + Pattern 2 from 13-RESEARCH.md) was followed verbatim.
## Issues Encountered
None.
## User Setup Required
None - no external service configuration, no migration, no environment changes. Pure TypeScript/DI addition.
## Next Phase Readiness
- `SourceRegistry` is ready to be wired into the Nest module (Plan 13-03 Task 3: register `DoeOpenDataAdapter` at boot) and consumed by `pollDueSources`'s fan-out (`activeAdapters()`/`get()`), replacing the current hardwired single-adapter injection.
- The `portals[]` contract is the exact shape Plan 13-04's config-driven `NetServerAdapter` (3 portals) and Plan 13-05's `CosinexAdapter` will implement — no further interface changes needed.
- INGEST-07 is now structurally satisfied at the registry layer; Plan 13-04/13-05 adapters will never be able to accidentally register `vergabe24`/`aumass` since the gate lives in `register()`, not in adapter-author discipline.
- No blockers.
---
*Phase: 13-scraping-adapters-cross-source-dedup*
*Completed: 2026-07-23*
## Self-Check: PASSED
All created/modified files verified present on disk; all 3 task commit hashes verified in git log.