153 lines
8.2 KiB
Markdown
153 lines
8.2 KiB
Markdown
<!-- status: done -->
|
||
<!-- started: 2026-05-18 -->
|
||
<!-- completed: 2026-05-18 -->
|
||
<!-- component: gateway | ui-nyla -->
|
||
<!-- lastReviewed: 2026-05-18 -->
|
||
<!-- verifiedAgainst: platform-core/modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py | platform-core/modules/routes/routeDataSources.py | ui-nyla/src/components/UnifiedDataBar/SourcesTab.tsx -->
|
||
|
||
# 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=<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:
|
||
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_<flag>`
|
||
|
||
### 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
|