wiki/c-work/4-done/2026-05-udb-polymorphic-refactor.md

141 lines
11 KiB
Markdown

<!-- status: validate -->
<!-- started: 2026-05-23 -->
<!-- component: platform-core | ui-nyla -->
<!-- lastReviewed: 2026-05-23 -->
# 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)