feat(14-02): add RssAdapter.parseFeed + 'rss' normalizer dispatch

Fixture-first RSS parsing (INGEST-04): parses live-captured
service.bund.de (pubDate present, numeric-HTML-entity titles) and
subreport-elvis (pubDate absent, CDATA titles) feed shapes into
RawTenderRecord[] via fast-xml-parser, mirroring the DoeOpenDataAdapter
config. SourceType extended with 'rss'; normalize() dispatches 'rss'
through the existing normalizeBag() path unchanged (D-04/D-05).

Rule 1 fix: fast-xml-parser only decodes the 5 predefined XML entities,
not numeric character references — added an explicit decode step so
service.bund.de titles ("Übermittlung...") render correctly
instead of leaking raw entity syntax.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:17:02 +02:00
parent a47c0c57ee
commit 3a96cbbbe6
7 changed files with 597 additions and 6 deletions
@@ -54,6 +54,7 @@ export class TenderNormalizerService {
switch (raw.sourceType) {
case 'ai-netserver':
case 'cosinex-dtvp':
case 'rss':
return this.normalizeBag(raw);
case 'doe-opendata':
default:
@@ -117,10 +118,13 @@ export class TenderNormalizerService {
/**
* Generic-bag path (gap closure): NetServer/cosinex-DTVP scraper adapters
* carry a flat `{title, buyerName, procedureType, legalFramework,
* deadlineAt}` bag in `raw.ocdsPayload` — there is no eForms/OCDS
* structure to inspect. `legalFramework` is deliberately NOT mapped:
* NormalizedTenderFields has no target field for it.
* (and, since Phase 14 Plan 02, the RssAdapter, INGEST-04) carry a flat
* `{title, buyerName, procedureType, legalFramework, deadlineAt}` bag in
* `raw.ocdsPayload` — there is no eForms/OCDS structure to inspect.
* `legalFramework` is deliberately NOT mapped: NormalizedTenderFields has
* no target field for it. RSS records always have `buyerName`/
* `procedureType`/`deadlineAt` null (baseline title/link/guid mapping
* only, D-04/D-05 thin-fields deferral).
*/
private normalizeBag(raw: RawTenderRecord): NormalizedTenderFields {
const bag = getBagPayload(raw.ocdsPayload);