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

8.2 KiB
Raw Blame History

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

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

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