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

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 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)

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: UdbNode ABC + Subklassen (synthetic, DS-Familie, FDS-Familie, FdsField)
  • _aggregateChildrenToParents geloescht (Logik jetzt polymorph in getEffectiveFlag(mode=aggregate))
  • _buildTree.py: Builder geben UdbNode-Instanzen zurueck; getChildrenForParents ohne instanceId-Parameter
  • routeUdb.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/children geloescht
  • FeatureDataSource-Schema: userId, workspaceInstanceId, scope entfernt
  • Frontend-Provider auf /api/udb/* umgestellt
  • Backend-Tests: test_udbNodes.py neu, test_buildTree.py + test_inheritFlags.py angepasst
  • Frontend-Tests: UdbSourcesProvider.test.ts angepasst
  • 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).
  • 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.md angelegt
  • TOPICS.md Eintrag ergaenzt
  • _CHANGELOG.md Eintrag ergaenzt
  • Dieses Dokument → c-work/4-done/ verschieben (nach Runtime-Smoke)