# 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: 1. User toggled SharePoint (Level 2) → `neutralize=true` 2. User toggled Folder1 (Level 3 unter SharePoint) → `neutralize=false` (explizit) 3. User klickt SharePoint nochmal → `neutralize=false` 4. User klickt SharePoint nochmal → `neutralize=true`. Erwartung: alle Folders auch true. **Aber**: Folder1 bleibt explizit `false` und ü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 1. Klare 3-Wertige Semantik: `null` (vererbt) | `true` | `false` für `neutralize` und `ragIndexEnabled` 2. Scope analog: `null` (vererbt) | `'personal'` | `'mandate'` | `'platform'` 3. Cascade beim Toggle resettet alle Descendants mit explizitem Wert für das **gleiche** Flag 4. Andere Flags des Descendants bleiben unangetastet (z.B. Toggle `neutralize` lässt `ragIndexEnabled` der Descendants in Ruhe) 5. Walker-Logik (RAG-Engine, Neutralisierungs-Pipeline) berechnet Effective-Value via Path-Traversal 6. 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]` (default `NULL` = inherit) - `ragIndexEnabled: Optional[bool]` (default `NULL` = inherit) - `scope: Optional[str]` (default `NULL` = 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=`. ### 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: 1. Setze `target.{flag} = value` für den geklickten Knoten 2. Finde alle Descendant-DataSources: `connectionId == target.connectionId AND path STARTSWITH target.path AND path != target.path` 3. Für jeden Descendant mit `descendant.{flag} IS NOT NULL` → setze `descendant.{flag} = NULL` 4. Audit-Log: `datasource_cascade_reset` mit Anzahl betroffener Records ### 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)`: ```python 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 | null` - `ragIndexEnabled: boolean | null` - `scope: string | null` Effective-Value-Computation analog zu Backend (path-basiert): ```typescript 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.py` und `datamodelFeatureDataSource.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 `platform-core/modules/serviceCenter/services/serviceUdb/_cascadeInherit.py`: - `cascadeResetDescendants(rootIf, dataSourceId, flag) -> int` — gibt Anzahl betroffener Records zurück - Audit-Logging via `recordModify` mit Reason `cascade_reset_` ### S3: PATCH-Endpoints anpassen - `routeDataSources.py`: `_updateDataSourceNeutralize`, `_updateDataSourceRagIndex`, `_updateDataSourceScope` - Body akzeptiert `value: Optional[bool|str]` - Bei nicht-null Wert: führe `cascadeResetDescendants` aus - 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 **effektivem** `ragIndexEnabled` (statt direktem) ### S5: Frontend - `connectionApi.ts`: `UdbDataSource` Interface auf `Optional`-Werte - `SourcesTab.tsx`: `_effectiveValue` Helper, `_findParentDs` Helper - `_TreeNodeView`: `effectiveValue` via Helper, nicht direkt `ds.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 Flags - `test_effectiveFlag.py`: Path-traversal returns Root-Default wenn alle Ancestors NULL sind - Bestehende RAG-Bootstrap-Tests: `ragIndexEnabled` Werte 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/platform-core/architecture.md`: neuer Abschnitt "DataSource Cascade-Inherit" - `_CHANGELOG.md` mit Begründung ## Risiken - **Breaking Change** für DataSource-DTO: Frontend muss `null` korrekt 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 → effectiveFlag` vor Walker-Run. - **Migrations-Race**: bestehende Records bleiben mit ihren Werten. Falls ein User einen DS auf `false` explizit gesetzt hat (in der alten Welt = "false default"), bleibt es nach Migration `false` explizit, nicht `NULL`. 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