wiki/c-work/4-done/2026-05-udb-cascade-inherit.md
2026-05-18 07:56:47 +02:00

153 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: done -->
<!-- started: 2026-05-18 -->
<!-- completed: 2026-05-18 -->
<!-- component: gateway | frontend-nyla -->
<!-- lastReviewed: 2026-05-18 -->
<!-- verifiedAgainst: gateway/modules/serviceCenter/services/serviceKnowledge/_inheritFlags.py | gateway/modules/routes/routeDataSources.py | frontend_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 `gateway/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/gateway/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