docs(10): phase verification — 5/5 must-haves verified, UAT pending docker rebuild
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
phase: 10-ausschreibungs-radar-foundation-d-e-ingestion
|
||||||
|
verified: 2026-07-21T09:32:05Z
|
||||||
|
status: human_needed
|
||||||
|
score: 5/5 must-haves verified
|
||||||
|
behavior_unverified: 0
|
||||||
|
overrides_applied: 0
|
||||||
|
re_verification: null
|
||||||
|
human_verification:
|
||||||
|
- test: "Als Admin im Marketplace 'Ausschreibungs-Radar' aktivieren (Tenant A), /modules/tender-radar oeffnen und bestaetigen, dass die Seite rendert (kein 404)."
|
||||||
|
expected: "Seite laedt mit Titel 'Ausschreibungs-Radar' + Platzhaltertext, kein 404."
|
||||||
|
why_human: "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: "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)."
|
||||||
|
expected: "Formular zeigt den zuvor gespeicherten Wert nach Reload; PUT-Aufruf schlaegt sich in der DB nieder."
|
||||||
|
why_human: "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: "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)."
|
||||||
|
expected: "Nach Rebuild erscheinen alle drei Boot-Logs; TenderSourcePollConfig hat genau 1 Zeile (sourceType='doe-opendata')."
|
||||||
|
why_human: "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 |
|
||||||
|
|
||||||
|
### Key Link Verification
|
||||||
|
|
||||||
|
| 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)_
|
||||||
Reference in New Issue
Block a user