7.9 KiB
UDB Cascade-Inherit für DataSource-Flags (neutralize, ragIndexEnabled, scope)
Beschreibung und Kontext
Das aktuelle UDB-Tree hat ein Konsistenz-Loch: Die drei Flags neutralize, ragIndexEnabled, scope auf DataSource sind als nicht-nullable Felder modelliert (bool/str mit Default false/'personal'). Das Frontend interpretiert "kein DataSource-Record" als "vererbt", aber sobald ein Knoten einen eigenen Record hat, bleibt sein Wert explizit gesetzt — auch dann, wenn der Parent später getoggelt wird.
Beispiel-Szenario, das aktuell falsch läuft:
- User toggled SharePoint (Level 2) →
neutralize=true - User toggled Folder1 (Level 3 unter SharePoint) →
neutralize=false(explizit) - User klickt SharePoint nochmal →
neutralize=false - User klickt SharePoint nochmal →
neutralize=true. Erwartung: alle Folders auch true. Aber: Folder1 bleibt explizitfalseund überschreibt die Vererbung.
Die User-Direktive: Bei Status-Änderung muss eine Cascade durch alle verschachtelten Subobjekte mit explizitem Wert für dieses Flag laufen — diese werden auf "vererbt" zurückgesetzt. Subobjekte, die bereits "vererbt" sind, werden ignoriert (sie folgen automatisch).
Ziele
- Klare 3-Wertige Semantik:
null(vererbt) |true|falsefürneutralizeundragIndexEnabled - Scope analog:
null(vererbt) |'personal'|'mandate'|'platform' - Cascade beim Toggle resettet alle Descendants mit explizitem Wert für das gleiche Flag
- Andere Flags des Descendants bleiben unangetastet (z.B. Toggle
neutralizelässtragIndexEnabledder Descendants in Ruhe) - Walker-Logik (RAG-Engine, Neutralisierungs-Pipeline) berechnet Effective-Value via Path-Traversal
- Kein manueller "Reset to inherit" im UI nötig — Vererbung wird ausschliesslich durch Parent-Toggle wiederhergestellt
Architektur-Entscheidungen
Datenmodell
DataSource (auch FeatureDataSource analog):
neutralize: Optional[bool](defaultNULL= inherit)ragIndexEnabled: Optional[bool](defaultNULL= inherit)scope: Optional[str](defaultNULL= inherit; legacy default'personal'ändern)
Migration: bestehende Records behalten ihre expliziten Werte (true/false/'personal'). Erst neue Records starten mit NULL. Damit ist die Migration nicht-destruktiv.
Effective-Value Computation (Walker + Frontend)
effective(node, flag) = node.flag if node.flag is not None
else effective(parent, flag)
else False # default für Root mit NULL
Path-Traversal: ein DS-Record gehört zu (connectionId, path). Parent-Pfade werden via String-Operationen ermittelt (/foo/bar → Parent /foo → Root /). Der Root einer Connection ist path='/' mit sourceType=<service>.
Cascade beim Toggle
PATCH-Endpoint:
PATCH /api/datasources/{id}/{flag}mit Body{value: bool | null}- Bei
null→ reset auf inherit (kein Cascade nötig, da der Knoten selbst dann erbt) - Bei
true/false→ Cascade:- Setze
target.{flag} = valuefür den geklickten Knoten - Finde alle Descendant-DataSources:
connectionId == target.connectionId AND path STARTSWITH target.path AND path != target.path - Für jeden Descendant mit
descendant.{flag} IS NOT NULL→ setzedescendant.{flag} = NULL - Audit-Log:
datasource_cascade_resetmit Anzahl betroffener Records
- Setze
Walker-Integration
RAG-Walker (subConnectorSync*.py) und Neutralisierungs-Pipeline rufen aktuell direkt ds.neutralize / ds.ragIndexEnabled ab. Das wird zu einem Helper getEffectiveFlag(ds, flag, allDataSources):
def getEffectiveFlag(ds, flag, allDsByConnection):
"""Path-traversal von ds aufwärts bis zum ersten DS mit explizitem Wert."""
current = ds
while current is not None:
value = getattr(current, flag)
if value is not None:
return value
current = _findParentDs(current, allDsByConnection)
return False # default
Wichtig: Der Walker iteriert über Files/Folders, nicht über DataSources. Aber die Entscheidung "neutralisieren ja/nein" wird pro Item getroffen. Der Item-Path wird auf den nächsten DS mit explizitem Flag-Wert gemappt. Falls kein expliziter Wert in der Kette, wird der Root-DS verwendet (path='/').
Frontend
UdbDataSource-Interface:
neutralize: boolean | nullragIndexEnabled: boolean | nullscope: string | null
Effective-Value-Computation analog zu Backend (path-basiert):
function _effectiveValue(ds, allDs, flag) {
if (ds[flag] !== null && ds[flag] !== undefined) return ds[flag];
const parent = _findParentDs(ds, allDs);
if (parent) return _effectiveValue(parent, allDs, flag);
return false;
}
UI-Verhalten beim Toggle:
- 2-state Toggle bleibt (true ↔ false). Cascade automatisch im Backend.
- Kein "Reset to inherit"-UI — User toggled stattdessen den Parent erneut.
Schritte
S1: Datenmodell + Migration
datamodelDataSource.pyunddatamodelFeatureDataSource.py:Optional[bool]/Optional[str]für die drei Flags- Migration-Skript
script_db_migrate_datasource_inherit.py(additiv-idempotent): ALTER COLUMN für die Spalten zu nullable. Bestehende Werte bleiben.
S2: Cascade-Helper im Backend
- Neue Datei
gateway/modules/serviceCenter/services/serviceUdb/_cascadeInherit.py:cascadeResetDescendants(rootIf, dataSourceId, flag) -> int— gibt Anzahl betroffener Records zurück
- Audit-Logging via
recordModifymit Reasoncascade_reset_<flag>
S3: PATCH-Endpoints anpassen
routeDataSources.py:_updateDataSourceNeutralize,_updateDataSourceRagIndex,_updateDataSourceScope- Body akzeptiert
value: Optional[bool|str] - Bei nicht-null Wert: führe
cascadeResetDescendantsaus - Bei null Wert: nur setzen, kein Cascade
S4: Walker-Integration
serviceKnowledge/_effectiveFlags.py(neu):getEffectiveFlag(ds, flag, allDsByConnection),getAncestorChain(ds, allDs)- Walker (RAG-Sync, Neutralisierungs-Pipeline) nutzen den Helper statt direkten
ds.neutralize-Zugriffs _loadRagEnabledDataSources: filtert nach effektivemragIndexEnabled(statt direktem)
S5: Frontend
connectionApi.ts:UdbDataSourceInterface aufOptional-WerteSourcesTab.tsx:_effectiveValueHelper,_findParentDsHelper_TreeNodeView:effectiveValuevia Helper, nicht direktds.neutralize ?? inheritedNeutralize- Toggle-Handler unverändert: PATCH macht Cascade automatisch
- Nach PATCH:
_fetchDataSources()damit Cascade-Reset im UI sichtbar
S6: Tests
test_cascade_inherit.py: Cascade-Reset löscht nur das Flag, nicht andere Flagstest_effectiveFlag.py: Path-traversal returns Root-Default wenn alle Ancestors NULL sind- Bestehende RAG-Bootstrap-Tests:
ragIndexEnabledWerte anpassen (explicit true statt implicit)
S7: Walker-Smoke
- Manuell: Connection mit Sub-Folders, Toggle Parent → Sub-Folder folgen ohne Cascade-DELETE
S8: Wiki + Changelog
b-reference/gateway/architecture.md: neuer Abschnitt "DataSource Cascade-Inherit"_CHANGELOG.mdmit Begründung
Risiken
- Breaking Change für DataSource-DTO: Frontend muss
nullkorrekt handeln. Mitigation: TypeScript-Types eng machen, alle Lese-Stellen prüfen. - Walker-Performance: Path-Traversal pro Item könnte O(N×depth) werden. Mitigation: Pre-Computed Map
path → effectiveFlagvor Walker-Run. - Migrations-Race: bestehende Records bleiben mit ihren Werten. Falls ein User einen DS auf
falseexplizit gesetzt hat (in der alten Welt = "false default"), bleibt es nach Migrationfalseexplizit, nichtNULL. Das ist OK — User-Wille bleibt respektiert.
Out of Scope
- 3-state UI (inherit/true/false) auf einem einzigen Toggle-Button — bleibt 2-state
- Settings-Modal "Reset to inherit"-Button — explizit verworfen vom User
- FeatureDataSource Cascade-Tests — analog zu DataSource, separater Folge-PR