| 13-scraping-adapters-cross-source-dedup |
02 |
backend |
| nestjs |
| di |
| security-gate |
| adapter-pattern |
|
| phase |
plan |
provides |
| 13-scraping-adapters-cross-source-dedup |
01 |
SourceType open union, TenderSourceAdapter interface (Phase 10 foundation) |
|
|
| 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 |
|
| 13-03-dedup-resolver-fan-out |
| 13-04-netserver-adapter |
| 13-05-cosinex-adapter |
|
| 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 |
|
|
| created |
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 |
| apps/api/src/tenders/adapters/doe-opendata.adapter.ts |
|
|
| 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 |
|
| SourceRegistry.get()/activeAdapters() is the seam Plan 13-03's pollDueSources fan-out will use instead of a hardwired single-adapter injection |
|
|
| id |
description |
requirement |
verification |
human_judgment |
| D1 |
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 |
INGEST-07 |
| kind |
ref |
status |
| unit |
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) |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D2 |
Legitimate adapters register and are retrievable via get(sourceType)/activeAdapters(); get() of an unregistered sourceType returns undefined without throwing |
INGEST-07 |
| kind |
ref |
status |
| unit |
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)' |
pass |
|
|
false |
|
| id |
description |
requirement |
verification |
human_judgment |
| D3 |
TenderSourceAdapter interface generalized with portals[]; DoeOpenDataAdapter declares portals = ['doe-opendata'] without behavior change; project-wide typecheck and full existing tenders test slice remain green |
INGEST-07 |
| kind |
ref |
status |
| unit |
cd apps/api && npx tsc --noEmit -p tsconfig.json (clean) |
pass |
|
| kind |
ref |
status |
| unit |
cd apps/api && npx vitest run src/tenders (18 files, 177/177 tests pass, includes unchanged doe-opendata.adapter.spec.ts 6/6) |
pass |
|
|
false |
|
|
20min |
2026-07-23 |
complete |