# UDB Polymorphic Refactor (Object-orientiert + Generic Router) ## Beschreibung und Kontext Nach dem Generic-Tree-Refactor (`2026-05-udb-generic-tree-refactor.md`) blieben strukturelle Schwaechen, die in der Praxis zu wiederholten "stuck toggle"-Bugs gefuehrt haben: - Die Flag-Logik war ueber `_buildTree.py`, `_inheritFlags.py`, vier PATCH-Routen, zwei Aggregatoren und das Frontend verstreut. Jede neue Node-Familie verlangte `if kind == ...`-Verzweigungen an mehreren Stellen. - `FeatureDataSource` trug ein `userId`, ein `workspaceInstanceId` und ein `scope`-Feld, obwohl FDS feature-owned ist und Scope ueber RBAC abgedeckt sein muss. Das verfuehrte zur Vermischung von Mandate-Visibility und User-Sharing. - Die UDB war ueber die `workspace`-Route ans Workspace-Feature verdrahtet, obwohl sie domaenenfrei und auf Systemebene nutzbar sein soll. - Ein Bug, bei dem `neutralize=true` auf einem DB-Feld nicht mehr zurueckschaltbar war, liess sich nicht punktuell beheben — Root Cause war fragmentierte Logik, nicht ein einzelnes Codestueck. User-Direktive: "keine optimistic hacks, keine workarounds und fallbacks, klare Logik, wo wir Fehler auch sehen, nicht verdecken". Ergo: harter Schnitt, polymorphes Backend, ein generischer Router. ## Ziele 1. **Polymorph statt fragmentiert.** Jeder Node-Typ ist eine Klasse mit klaren Hooks (`canEdit`, `getEffectiveFlag`, `setFlag`, `getLogicalChildren`, `toDict`). Neue Kinds erweitern die Hierarchie statt zentrale Switches. 2. **Eine generische API.** `POST /api/udb/tree/children` fuer Walks, `POST /api/udb/node/{key}/flag/{flag}` fuer Persistenz. Adressiert via Tree-Key (funktioniert auch fuer virtuelle Nodes wie `fdsField`). 3. **UDB feature-entkoppelt.** Eigene `/api/udb`-Route. Keine Bindung mehr ans Workspace-Feature. 4. **Klare Ownership-Semantik.** DataSource = user-private mit `scope`. FeatureDataSource = feature-owned, RBAC-gated, kein `userId`/`workspaceInstanceId`/`scope`. 5. **RBAC explizit verankert.** Flag-Edits auf FDS verlangen Feature-Admin (`Role.roleLabel.endswith("-admin")` auf der `featureInstanceId`). SysAdmin/PlatformAdmin sind kein Bypass. ## Nicht-Ziele - Kein Kompatibilitaets-Layer. Alte Endpoints werden geloescht, nicht gespiegelt. - Keine Aenderung an der UDB-UI-Optik (Symbole, Klick-Semantik, mixed-Anzeige) — diese stammt aus dem vorherigen Refactor. - Keine Erweiterung der RAG-Pipeline. Bootstrap-Job laeuft weiter, jetzt aber per `featureInstanceId` (FDS hat keinen `workspaceInstanceId` mehr). ## Betroffene Module - **Platform-Core (Backend):** - Neu: `modules/serviceCenter/services/serviceKnowledge/udbNodes.py` (Klassenhierarchie) - Neu: `modules/routes/routeUdb.py` (Generic Router) - Refactored: `modules/serviceCenter/services/serviceKnowledge/_buildTree.py` (Builder geben `UdbNode`-Instanzen zurueck) - Refactored: `modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py` (FDS-Vererbung ohne `scope`, Coordinate `featureInstanceId` statt `workspaceInstanceId`) - Refactored: `modules/datamodels/datamodelFeatureDataSource.py` (entfernt: `userId`, `workspaceInstanceId`, `scope`) - Refactored: `modules/routes/routeDataSources.py` (4 PATCH-Endpoints entfernt; nur `/settings` und `/cost-estimate` bleiben) - Refactored: `modules/features/workspace/routeFeatureWorkspace.py` (alter `/tree/children`-Endpoint entfernt; FDS-Create ohne `userId`/`workspaceInstanceId`) - Refactored: `modules/routes/routeRagInventory.py` (Reindex via `featureInstanceId`) - Refactored: `modules/serviceCenter/services/serviceKnowledge/subFeatureBootstrap.py` (Payload `featureInstanceId`, kein FDS-`userId` mehr → `"system"`) - Refactored: `modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py` (FDS-Lookup ohne `workspaceInstanceId`) - App-Registration: `app.py` (`app.include_router(udbRouter)`) - **UI-Nyla (Frontend):** - Refactored: `src/components/UnifiedDataBar/UdbSourcesProvider.tsx` (alle Flag-Patches gehen ueber `POST /api/udb/node/{key}/flag/{flag}`, `_patchFieldNeutralize` entfernt) - Refactored: `src/api/connectionApi.ts` (alte `patchDataSourceRagIndex`-Helper entfernt) - Docstring: `src/components/UnifiedDataBar/SourcesTab.tsx` - **Tests:** - Neu: `platform-core/tests/unit/services/test_udbNodes.py` (umfassende Coverage der Klassenhierarchie) - Refactored: `platform-core/tests/unit/services/test_buildTree.py` - Refactored: `platform-core/tests/unit/services/test_inheritFlags.py` - Refactored: `ui-nyla/src/components/UnifiedDataBar/__tests__/UdbSourcesProvider.test.ts` - **Wiki:** - Neu: `wiki/b-reference/platform/unified-data-bar.md` (kanonische Reference) - Updates: `wiki/b-reference/platform/neutralization.md`, `wiki/b-reference/platform/rbac.md`, `wiki/TOPICS.md`, `wiki/c-work/_CHANGELOG.md` - **DB-Migration:** nein (Schema-Felder am Model entfernt; bestehende DB-Spalten bleiben unbenutzt liegen — keine Datenverluste, keine Live-Migration noetig). ## Entscheidungen | Datum | Entscheidung | Begruendung | |---|---|---| | 2026-05-23 | Hart-Cut der 4 PATCH-Routen | "Keine Workarounds, keine optimistic hacks." User-Direktive. | | 2026-05-23 | Tree-Key als API-Adresse statt `sourceId` | Funktioniert auch fuer virtuelle Nodes wie `fdsField` (kein eigener DB-Record). | | 2026-05-23 | Eigene `/api/udb`-Route, weg von `/api/workspace` | UDB ist feature-unabhaengig; sie kann auch auf System-Level ohne Feature-Kontext verwendet werden. | | 2026-05-23 | FDS verliert `userId`, `workspaceInstanceId`, `scope` | FDS ist feature-owned. Visibility kommt aus RBAC, Edit aus Feature-Admin. Scope-Vermischung erzeugte den Field-Toggle-Bug. | | 2026-05-23 | RBAC fuer FDS-Edits: `roleLabel.endswith('-admin')` | Pro Feature-Modul gibt es eigene Admin-Rollen (`workspace-admin`, `trustee-admin`, ...). Suffix-Match statt Allow-List. | | 2026-05-23 | Kein SysAdmin/PlatformAdmin-Bypass auf UDB-Flag-Edits | Daten-Verantwortungs-Akt, kein Plattform-Operations-Akt. | | 2026-05-23 | Visibility-Cascade fuer geteilte DataSources nicht eingebaut | Wuerde die Berechtigungsmatrix der externen Provider (SharePoint, Drive) ueberlagern. Sharing bleibt strikt read-only fuer Empfaenger. | ## Architektur (Kurzfassung) ```mermaid flowchart LR Frontend["UdbSourcesProvider.tsx"] -->|"POST /api/udb/tree/children"| Router["routeUdb.py"] Frontend -->|"POST /api/udb/node/{k}/flag/{f}"| Router Router -->|"buildNodeForKey(k)"| Nodes["udbNodes.py - UdbNode + Subklassen"] Router -->|"getChildrenForParents(...)"| Builder["_buildTree.py"] Builder --> Nodes Nodes --> InheritFlags["_inheritFlags.py - Path-Traversal + Cascade"] Nodes --> Models["DataSource / FeatureDataSource"] ``` Volle Architektur und API-Details: `b-reference/platform/unified-data-bar.md`. ## Umsetzungs-Checkliste - [x] `udbNodes.py`: `UdbNode` ABC + Subklassen (synthetic, DS-Familie, FDS-Familie, FdsField) - [x] `_aggregateChildrenToParents` geloescht (Logik jetzt polymorph in `getEffectiveFlag(mode=aggregate)`) - [x] `_buildTree.py`: Builder geben `UdbNode`-Instanzen zurueck; `getChildrenForParents` ohne `instanceId`-Parameter - [x] `routeUdb.py`: `/api/udb/tree/children` + `/api/udb/node/{key}/flag/{flag}` - [x] Alte PATCH-Routen geloescht (`/scope`, `/neutralize`, `/rag-index`, `/neutralize-fields`); alter `/api/workspace/{id}/tree/children` geloescht - [x] `FeatureDataSource`-Schema: `userId`, `workspaceInstanceId`, `scope` entfernt - [x] Frontend-Provider auf `/api/udb/*` umgestellt - [x] Backend-Tests: `test_udbNodes.py` neu, `test_buildTree.py` + `test_inheritFlags.py` angepasst - [x] Frontend-Tests: `UdbSourcesProvider.test.ts` angepasst - [x] Wiki: neue Reference-Page `unified-data-bar.md`; Updates auf `neutralization.md`, `rbac.md`, `TOPICS.md`, `_CHANGELOG.md` - [ ] Runtime-Smoke: lokal Backend starten, je einen Toggle pro Familie testen (`fdsField`, `fdsTable`, `ds-folder`, `connection`) ## Akzeptanzkriterien | # | Kriterium (Given-When-Then) | Prio | |---|---|---| | 1 | Given ein DS-File-Node mit explizitem `neutralize=true` und ein Parent-Folder ohne expliziten Wert, When der User auf das mixed-Symbol des Parent klickt, Then setzt das Backend `neutralize=false` am Parent und cascade-resetted die expliziten Werte aller Descendants. | must | | 2 | Given ein User ohne `-admin`-Rolle auf der Feature-Instanz, When er versucht `neutralize` auf einer FDS-Table zu togglen, Then antwortet der Server mit 403 und schreibt einen Audit-Eintrag. | must | | 3 | Given eine FDS mit `neutralizeFields=["email"]`, When der User auf das Feld `email` klickt und neutralize toggelt, Then wird `email` aus der Liste entfernt (`neutralizeFields=[]`) und der naechste Refetch zeigt `effectiveNeutralize=false` auf dem Field. | must | | 4 | Given drei Files unter demselben Folder, alle mit explizitem `neutralize=true`, When der Folder via Refetch geladen wird, Then liefert das Backend `effectiveNeutralize=true` (nicht "mixed") am Folder. | must | | 5 | Given zwei Files unter demselben Folder, eines mit `neutralize=true` und eines mit `neutralize=false`, When der Folder via Refetch geladen wird, Then liefert das Backend `effectiveNeutralize="mixed"` am Folder. | must | | 6 | Given ein POST auf `/api/udb/node/{k}/flag/scope` mit `value="global"` von einem nicht-SysAdmin, When der Request validiert wird, Then 403 und kein DB-Write. | must | ## Testplan | ID | AC | Art | Automatisiert | Repo-Pfad | Status | |----|----|-----|---------------|-----------|--------| | T1 | 1,4,5 | unit | ja | `platform-core/tests/unit/services/test_udbNodes.py::TestGetEffectiveFlag` | done | | T2 | 2 | unit | ja | `platform-core/tests/unit/services/test_udbNodes.py::TestCanEditFdsFeatureAdmin`, `TestIsFeatureAdmin` | done | | T3 | 3 | unit | ja | `platform-core/tests/unit/services/test_udbNodes.py::TestSetFlag` (FdsFieldNode-Branch) | done | | T4 | 1 | unit | ja | `platform-core/tests/unit/services/test_inheritFlags.py` (cascade-reset) | done | | T5 | 6 | manuell | nein | Runtime-Smoke via UI/curl | pending | | T6 | alle | runtime | nein | Lokaler Smoke-Test mit `ui-nyla` + `platform-core` | pending | ## Risiken / offene Punkte - **DB-Spalten am `FeatureDataSource` bleiben physisch in der Tabelle liegen** (`userId`, `workspaceInstanceId`, `scope`). Solange die ORM-Modelle sie nicht mehr referenzieren, sind sie inert. Eine spaetere DROP-Migration ist optional. - **Runtime-Smoke pendet.** Vor dem Verschieben in `4-done` ist mindestens ein End-to-End-Test pro Node-Familie erforderlich (Edit + Read-back via UI). ## Links - Reference: `b-reference/platform/unified-data-bar.md` - Vorgaenger: `c-work/4-done/2026-05-udb-generic-tree-refactor.md`, `c-work/4-done/2026-05-udb-cascade-inherit.md` - Toggle-Spec-Recovery (Symptome, die den Refactor erzwungen haben): `c-work/3-validate/2026-05-udb-toggle-spec-recovery.md` ## Abschluss - [ ] Runtime-Smoke gruen - [x] `b-reference/platform/unified-data-bar.md` angelegt - [x] `TOPICS.md` Eintrag ergaenzt - [x] `_CHANGELOG.md` Eintrag ergaenzt - [ ] Dieses Dokument → `c-work/4-done/` verschieben (nach Runtime-Smoke)