11 KiB
11 KiB
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 verlangteif kind == ...-Verzweigungen an mehreren Stellen. FeatureDataSourcetrug einuserId, einworkspaceInstanceIdund einscope-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=trueauf 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
- 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. - Eine generische API.
POST /api/udb/tree/childrenfuer Walks,POST /api/udb/node/{key}/flag/{flag}fuer Persistenz. Adressiert via Tree-Key (funktioniert auch fuer virtuelle Nodes wiefdsField). - UDB feature-entkoppelt. Eigene
/api/udb-Route. Keine Bindung mehr ans Workspace-Feature. - Klare Ownership-Semantik. DataSource = user-private mit
scope. FeatureDataSource = feature-owned, RBAC-gated, keinuserId/workspaceInstanceId/scope. - RBAC explizit verankert. Flag-Edits auf FDS verlangen Feature-Admin (
Role.roleLabel.endswith("-admin")auf derfeatureInstanceId). 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 keinenworkspaceInstanceIdmehr).
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 gebenUdbNode-Instanzen zurueck) - Refactored:
modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py(FDS-Vererbung ohnescope, CoordinatefeatureInstanceIdstattworkspaceInstanceId) - Refactored:
modules/datamodels/datamodelFeatureDataSource.py(entfernt:userId,workspaceInstanceId,scope) - Refactored:
modules/routes/routeDataSources.py(4 PATCH-Endpoints entfernt; nur/settingsund/cost-estimatebleiben) - Refactored:
modules/features/workspace/routeFeatureWorkspace.py(alter/tree/children-Endpoint entfernt; FDS-Create ohneuserId/workspaceInstanceId) - Refactored:
modules/routes/routeRagInventory.py(Reindex viafeatureInstanceId) - Refactored:
modules/serviceCenter/services/serviceKnowledge/subFeatureBootstrap.py(PayloadfeatureInstanceId, kein FDS-userIdmehr →"system") - Refactored:
modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py(FDS-Lookup ohneworkspaceInstanceId) - App-Registration:
app.py(app.include_router(udbRouter))
- Neu:
- UI-Nyla (Frontend):
- Refactored:
src/components/UnifiedDataBar/UdbSourcesProvider.tsx(alle Flag-Patches gehen ueberPOST /api/udb/node/{key}/flag/{flag},_patchFieldNeutralizeentfernt) - Refactored:
src/api/connectionApi.ts(altepatchDataSourceRagIndex-Helper entfernt) - Docstring:
src/components/UnifiedDataBar/SourcesTab.tsx
- Refactored:
- 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
- Neu:
- 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
- Neu:
- 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)
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
udbNodes.py:UdbNodeABC + Subklassen (synthetic, DS-Familie, FDS-Familie, FdsField)_aggregateChildrenToParentsgeloescht (Logik jetzt polymorph ingetEffectiveFlag(mode=aggregate))_buildTree.py: Builder gebenUdbNode-Instanzen zurueck;getChildrenForParentsohneinstanceId-ParameterrouteUdb.py:/api/udb/tree/children+/api/udb/node/{key}/flag/{flag}- Alte PATCH-Routen geloescht (
/scope,/neutralize,/rag-index,/neutralize-fields); alter/api/workspace/{id}/tree/childrengeloescht FeatureDataSource-Schema:userId,workspaceInstanceId,scopeentfernt- Frontend-Provider auf
/api/udb/*umgestellt - Backend-Tests:
test_udbNodes.pyneu,test_buildTree.py+test_inheritFlags.pyangepasst - Frontend-Tests:
UdbSourcesProvider.test.tsangepasst - Wiki: neue Reference-Page
unified-data-bar.md; Updates aufneutralization.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
FeatureDataSourcebleiben 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-doneist 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
b-reference/platform/unified-data-bar.mdangelegtTOPICS.mdEintrag ergaenzt_CHANGELOG.mdEintrag ergaenzt- Dieses Dokument →
c-work/4-done/verschieben (nach Runtime-Smoke)