Files
tessera-ctl/.planning/phases/10-ausschreibungs-radar-foundation-d-e-ingestion/10-VERIFICATION.md
T
2026-07-21 11:34:07 +02:00

16 KiB
Raw Blame History

phase, verified, status, score, behavior_unverified, overrides_applied, re_verification, human_verification
phase verified status score behavior_unverified overrides_applied re_verification human_verification
10-ausschreibungs-radar-foundation-d-e-ingestion 2026-07-21T09:32:05Z human_needed 5/5 must-haves verified 0 0 null
test expected why_human
Als Admin im Marketplace 'Ausschreibungs-Radar' aktivieren (Tenant A), /modules/tender-radar oeffnen und bestaetigen, dass die Seite rendert (kein 404). Seite laedt mit Titel 'Ausschreibungs-Radar' + Platzhaltertext, kein 404. Erfordert einen echten Browser-Roundtrip durch Marketplace-Aktivierung -> lazy-loaded Modulseite. Der lokale API/Web-Container laeuft noch auf dem VOR-Phase-10-Image (siehe Anti-Pattern-Sektion) — TendersModule/TendersController sind im laufenden Container nicht gemappt. Ein Rebuild/Neustart des Stacks liegt laut Projekt-Konvention beim User, nicht beim Verifier.
test expected why_human
Als Admin /modules/tender-radar/settings oeffnen, Intervall aendern, speichern, Seite neu laden und pruefen, dass der neue Wert persistiert wurde (echter GET/PUT-Roundtrip gegen die Plan-05-Route). Formular zeigt den zuvor gespeicherten Wert nach Reload; PUT-Aufruf schlaegt sich in der DB nieder. Component-Tests mocken tender-radar-api.ts vollstaendig (10-06 SUMMARY selbst als human_judgment:true dokumentiert). Kein Live-Container mit dem neuen Code laeuft aktuell, siehe oben.
test expected why_human
Container-Rebuild durchfuehren und pruefen, dass TendersModule beim Boot tatsaechlich onModuleInit() durchlaeuft (Log-Zeile 'Ausschreibungs-Radar module seeded in registry' + 'doe-opendata poll config seeded' + TenderSchedulerService-Log erscheinen; TenderSourcePollConfig-Tabelle enthaelt danach genau 1 Zeile). Nach Rebuild erscheinen alle drei Boot-Logs; TenderSourcePollConfig hat genau 1 Zeile (sourceType='doe-opendata'). Der laufende Container ist aelter als Phase 10 und hat diesen Code nie ausgefuehrt (siehe Anti-Pattern-Sektion) — die Live-DB-Tabelle TenderSourcePollConfig ist aktuell leer (0 Zeilen), obwohl der Code einen Boot-Seed garantiert. Ohne Rebuild ist dies nicht abschliessend pruefbar; kein Docker-Deploy durch den Verifier gemaess Projekt-Konvention.

Phase 10: Ausschreibungs-Radar Foundation & DÖE Ingestion Verification Report

Phase Goal: The platform ingests German public tenders from the DÖE OpenData API on a shared, multi-tenant-safe schedule into a normalized, platform-global schema, and the module can be activated per tenant from the marketplace. Verified: 2026-07-21T09:32:05Z Status: human_needed Re-verification: No — initial verification

Zusammenfassung

Alle 5 ROADMAP-Erfolgskriterien sind auf Code-/Schema-/Testebene durch eigene Nachpruefung (nicht nur SUMMARY-Behauptungen) bestaetigt: Schema ist global (kein tenantId), Marketplace-Self-Seed + Web-Whitelist existieren und sind verdrahtet, Content-Hash-Change-Detection ist per echtem Integrationstest bewiesen, der Scheduler ist ein einziger globaler Cron-Job (kein findFirst/activeTenantId), und der Zwei-Mandanten-Sicherheitstest treibt tatsaechlich den echten ModuleRegistryService.activateForTenant()-Pfad (kein Tautologie-Stub). Beide Testsuiten (API 74/74, Web 111/111) sind gruen, tsc --noEmit ist sauber in beiden Apps, die Migration ist live in der DB angewendet.

Der einzige Befund, der den Status von passed auf human_needed senkt: der lokale Docker-Stack laeuft noch auf dem VOR-Phase-10-Image — TendersModule/TendersController sind im aktuell laufenden Container nicht registriert (kein TendersController-Routen-Log, keine Seed-Log-Zeile), und TenderSourcePollConfig ist in der Live-DB leer (0 Zeilen), obwohl der Code einen garantierten Boot-Seed vorsieht. Das ist kein Code-Defekt — es ist exakt die vom Executor selbst dreimal dokumentierte Luecke (10-02 D2/D3, 10-06 D3: "human_judgment: true ... Rebuild liegt beim User"). Es bedeutet aber, dass der End-to-End-Browser-Roundtrip (Marketplace-Aktivierung -> Seite laedt; Settings-Formular -> Persistenz) bislang nirgends tatsaechlich beobachtet wurde — weder von Claude noch vom User. Das gehoert vor Phase-11-Start in eine kurze manuelle UAT-Runde.

Goal Achievement

Observable Truths

# Truth Status Evidence
1 Admin findet "Ausschreibungs-Radar" im Marketplace und kann es pro Mandant aktivieren (wie DKV/Cert-Manager) ✓ VERIFIED (Code) / siehe Human-Item 1 (Live) tenders.seed.ts seedt slug:'tender-radar', isSystem:true; app.module.ts importiert TendersModule (grep=2); Marketplace-Seite laedt Module generisch via GET /modules (kein hardcodierter Modul-Katalog) — apps/web/src/app/(portal)/marketplace/page.tsx:55; module-loader.ts hat den Pflicht-Whitelist-Eintrag 'tender-radar' (Zeile 50-54); tender-radar/page.tsx existiert und default-exportiert. tenders.seed.spec.ts (1 Test) gruen
2 Neue DÖE-Ausschreibungen erscheinen als normalisierte Records (Titel, Buyer, CPV, Region/PLZ, Frist, Wert, Verfahrensart, Quell-URL) nach einem Poll — plattform-global, nicht mandantengebunden ✓ VERIFIED Live-Schema-Introspektion via Prisma (information_schema.columns) bestaetigt: Tender-Tabelle hat keine tenantId-Spalte, alle geforderten Felder vorhanden (title, buyerName, cpvCodes, region, plz, deadlineAt, estimatedValue, procedureType, sourceUrl). DoeOpenDataAdapter/TenderNormalizerService gegen ECHTE, live von oeffentlichevergabe.de gezogene Fixture-ZIPs (51KB/16KB, 8 reale Notices) getestet — 6/6 + 6/6 Tests gruen, inkl. exakter D-02-Filter-Zaehlung (4 von 8) und eForms-primaerer Deadline-Wiederherstellung wo OCDS null ist
3 Eine geaenderte DÖE-Ausschreibung aktualisiert den bestehenden Record via Content-Hash statt eines Duplikats (SCHEMA-02) ✓ VERIFIED tender-ingestion.service.ts: prisma.tender.upsert({ where: { dedupKey } }); tender-ingestion.service.spec.ts — expliziter Test "updates the existing row in place when the same dedupKey reappears with a changed contentHash" pruft Store-Groesse bleibt 1 UND geaenderte Felder werden uebernommen; separater Test fuer identische Notiz (kein Duplikat). Beide gruen
4 DÖE wird einmal auf einem gemeinsamen, admin-konfigurierbaren Intervall gepollt, unabhaengig von der Anzahl aktiver Mandanten (poll-once-fan-out-many, nicht DKV-findFirst()) ✓ VERIFIED tender-scheduler.service.ts: genau EIN Cron-Job (JOB_NAME='tender-doe-poll'), onModuleInit() nutzt findUnique({where:{sourceType}}) (kein findFirst), setInterval(intervalMin) hat KEIN Tenant-Argument. Grep bestaetigt: activeTenantId = 0 Treffer, findFirst = 0 Treffer, findUnique >= 1. Admin-UI (SourceConfigForm + PUT /source-config) treibt scheduler.setInterval()/stopJob() live ohne Neustart — bewiesen durch tenders.controller.spec.ts
5 Aktivierung fuer einen 2. Mandanten dupliziert Ingestion nicht, triggert keinen redundanten Poll, beeinflusst Mandant-1-Daten nicht ✓ VERIFIED tender-scheduler.service.spec.ts treibt den ECHTEN (nicht gemockten) ModuleRegistryService.activateForTenant() gegen ein Fake-Prisma — eigene Nachpruefung von activateForTenant() im Quellcode bestaetigt: die Funktion beruehrt ausschliesslich module/tenantModuleActivation-Tabellen, NICHTS DÖE-Spezifisches. Test asserts nach 2. Aktivierung: addCronJob weiterhin 1x, pollDueSources 0x zusaetzlich aufgerufen, tender.count() bleibt 0 — echter Integrationsbeweis, keine Tautologie

Score: 5/5 truths verified (0 present, behavior-unverified)

Required Artifacts

Artifact Expected Status Details
apps/api/prisma/schema.prisma (Tender, TenderSourcePollConfig) Global Tender-Tabelle, Singleton-Config ✓ VERIFIED Live-DB-Introspektion bestaetigt Spalten + Fehlen von tenantId; prisma migrate status -> "Database schema is up to date!"
apps/api/src/tenders/tenders.module.ts Self-seeding Modul, Provider-Registrierung ✓ VERIFIED OnModuleInit ruft seedTendersModule() + upsert des Singleton-Configs; alle Services in providers
apps/api/src/tenders/tenders.seed.ts Marketplace-Seed ✓ VERIFIED slug:'tender-radar', isSystem:true, bilinguale Beschreibung
apps/web/src/lib/module-loader.ts Whitelist-Eintrag tender-radar ✓ VERIFIED Zeile 50-54, dynamic(() => import(...))
apps/web/src/app/(portal)/modules/tender-radar/page.tsx Modul-Seite (MVP-Stub) ✓ VERIFIED Existiert, default export, dokumentierter Platzhalter fuer Phase 11
apps/api/src/tenders/adapters/doe-opendata.adapter.ts Fetch/Extract/Parse/D-02-Filter ✓ VERIFIED Echte Fixtures, exakte Zaehl-Assertion (4/8), Zip-Bomb-Ceiling-Test gruen
apps/api/src/tenders/tender-normalizer.service.ts Feld-Mapping, dedupKey, contentHash ✓ VERIFIED eForms-primaere Deadline-Wiederherstellung bewiesen
apps/api/src/tenders/tender-ingestion.service.ts Day-Cursor-Gate, Upsert, Retention ✓ VERIFIED 7 Tests gruen inkl. No-Op-Gate, SCHEMA-02, D-05-Retention (Null-Deadline nie geloescht)
apps/api/src/tenders/tender-scheduler.service.ts Einziger globaler Cron ✓ VERIFIED Grep-Gates gruen (activeTenantId=0, findFirst=0)
apps/api/src/tenders/tenders.controller.ts Global Read (ModuleGuard) + Admin Roles ✓ VERIFIED @UseModule('tender-radar') auf GET-Routen, @Roles(ADMIN,SUPER_ADMIN) auf beiden Source-Config-Routen, kein tenantId im Read-Where
apps/web/src/lib/tender-radar-api.ts + SourceConfigForm.tsx + settings/page.tsx Admin-UI fuer Poll-Intervall ✓ VERIFIED fetchSourceConfig/saveSourceConfig vorhanden, Formular mit Bounds 5-1440, 4 Component-Tests gruen
From To Via Status Details
TendersModule.onModuleInit() ModuleRegistryService.seedModule() direkter Aufruf ✓ WIRED Code bestaetigt, Test bestaetigt Call-Shape
module-loader.ts MODULE_REGISTRY['tender-radar'] tender-radar/page.tsx dynamic(() => import(...)) ✓ WIRED Datei existiert, Import-Pfad korrekt
TenderSchedulerService Cron-Tick TenderIngestionService.pollDueSources() .catch(logErr) im CronJob-Callback ✓ WIRED Code-Inspektion bestaetigt
prisma.tender.upsert({where:{dedupKey}}) SCHEMA-02 Change-Detection Upsert-Semantik ✓ WIRED Test beweist Update-statt-Duplikat
PUT /source-config TenderSchedulerService.setInterval()/stopJob() direkter Aufruf im Controller ✓ WIRED Controller-Test beweist beide Zweige
SourceConfigForm GET/PUT /modules/tender-radar/source-config fetchSourceConfig/saveSourceConfig ✓ WIRED (Component-Test-Ebene) Live-Roundtrip nicht beobachtet — siehe Human-Item 2

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
TendersController.listTenders() items/total prisma.tender.findMany/count (echte DB-Query, kein statischer Return) Ja (Query-basiert) ✓ FLOWING
SourceConfigForm pollIntervalMin/isActive fetchSourceConfig() -> GET /source-config (echte Prisma-Query im Controller) Ja (kein Hardcoded-Leerwert im Call-Pfad) ✓ FLOWING (mock-getestet, Live-Pfad siehe Human-Item 2)
tender-radar/page.tsx — keine (statischer Platzhaltertext) N/A Bewusster, dokumentierter MVP-Stub fuer Phase 11 (kein verdeckter Stub — im Plan/Summary explizit als solcher deklariert)

Behavioral Spot-Checks

Behavior Command Result Status
API-Testsuite vollstaendig gruen pnpm --filter @tessera/api test (DATABASE_URL gesetzt) 9 Dateien, 74/74 Tests ✓ PASS
Web-Testsuite vollstaendig gruen pnpm --filter @tessera/web test 20 Dateien, 111/111 Tests ✓ PASS
API tsc --noEmit sauber cd apps/api && pnpm exec tsc --noEmit -p tsconfig.json Exit 0, keine Ausgabe ✓ PASS
Migration live angewendet prisma migrate status "Database schema is up to date!" (12 Migrationen) ✓ PASS
Tender-Tabelle hat keine tenantId-Spalte (Live-DB) information_schema.columns Query via Prisma 21 Spalten, tenantId nicht enthalten ✓ PASS
Zwei-Mandanten-Test treibt echten Aktivierungspfad Quellcode-Review von activateForTenant() Beruehrt nur module/tenantModuleActivation, nichts DÖE-Spezifisches ✓ PASS
Zip-Bomb-Ceiling-Test Teil der Adapter-Suite 374ms, gruen ✓ PASS

Requirements Coverage

Requirement Source Plan Description Status Evidence
CONFIG-01 10-02 Modul im Marketplace registriert und pro Mandant aktivierbar ✓ SATISFIED (Code) Seed + Whitelist + generische Marketplace-Seite; Live-Klick-Pfad -> Human-Item 1
INGEST-01 10-03 DÖE OpenData-API zeitgesteuert abgerufen (eForms/OCDS, auth-frei) ✓ SATISFIED Adapter mit nativem fetch, echte Fixtures, D-02-Filter bewiesen
INGEST-06 10-04, 10-05, 10-06 Poll-once-fan-out-many, Intervall admin-konfigurierbar (inkl. UI) ✓ SATISFIED Scheduler + Controller + Web-Formular alle vorhanden und verdrahtet; Live-Roundtrip -> Human-Item 2
SCHEMA-01 10-01, 10-03 Einheitliches, OCDS-orientiertes, plattform-globales Schema ✓ SATISFIED Schema-Introspektion bestaetigt globale Tabelle + alle Felder
SCHEMA-02 10-04 Content-Hash-Change-Detection statt Duplikat ✓ SATISFIED Upsert-Test beweist Update-in-Place

Keine verwaisten (orphaned) Requirements gefunden — alle 5 in REQUIREMENTS.md gelisteten IDs fuer Phase 10 sind in mindestens einem Plan deklariert und durch Coverage-Eintraege belegt.

Anti-Patterns Found

File Line Pattern Severity Impact
— — Keine TBD/FIXME/XXX/TODO/HACK/PLACEHOLDER-Marker in apps/api/src/tenders/** oder den Tender-Radar-Web-Dateien gefunden — —
apps/web/.../tender-radar/page.tsx ganze Datei Hartcodierter deutscher Platzhaltertext, keine echten Daten ℹ️ Info Explizit im Plan 10-02 als MVP-Stub deklariert, mit Doc-Kommentar "replace this whole component in Phase 11" — kein verdeckter Stub, akzeptiert
Live-Docker-Container tessera-ctl-api-1 Boot-Logs Container-Image ist AELTER als Phase 10 — keine TendersController/TendersModule-Routen im Boot-Log, TenderSourcePollConfig-Tabelle in der Live-DB leer (0 Zeilen) trotz garantiertem Boot-Seed im Code ⚠️ Warning Kein Code-Defekt (Migration + Code sind korrekt und durch Tests bewiesen); aber es bedeutet, dass niemand — weder Executor noch Verifier — den Boot-Seed oder einen Live-HTTP-Request gegen die neuen Routen tatsaechlich beobachtet hat. Rebuild liegt laut Projekt-Konvention beim User

Human Verification Required

Siehe YAML-Frontmatter human_verification fuer die drei Items (Marketplace-Aktivierung + Seiten-Load, Settings-Formular-Roundtrip, Boot-Seed-Bestaetigung nach Rebuild). Alle drei sind bereits vom Executor selbst als offene UAT-Punkte dokumentiert (10-02 SUMMARY D2/D3, 10-06 SUMMARY D3) und werden hier bestaetigt, nicht neu erfunden.

Gaps Summary

Keine Gaps auf Code-Ebene gefunden. Alle 5 ROADMAP-Erfolgskriterien sind durch eigene Nachpruefung (Live-Schema-Introspektion, Quellcode-Review, tatsaechliches Ausfuehren beider Testsuiten, Pruefung des echten activateForTenant()-Pfads) bestaetigt — nicht nur durch SUMMARY-Behauptungen uebernommen. Der Status ist human_needed statt passed ausschliesslich wegen des veralteten laufenden Docker-Images, das eine echte Browser-Bestaetigung des End-to-End-Flows verhindert. Dies ist keine Code-Luecke, sondern eine Infrastruktur-/Deployment-Luecke, die dem Projekt-Konvention entsprechend beim User liegt (Rebuild). Empfehlung: kurzer manueller UAT-Durchlauf nach Stack-Rebuild, dann kann Phase 11 starten.


Verified: 2026-07-21T09:32:05Z Verifier: Claude (gsd-verifier)