TGV · Master-Konzept · Betrieb & Architektur · Stand 2026-07-13
Multi-Mandant
Wie TGV viele Kunden trägt: ein Datenraum pro Mandant, harte Wände, ein Fahrplan zum Mehr-Mandanten-Betrieb — und ein Mandanten-Erlebnis, das sich pro Kunde anfühlt wie SEIN Produkt (Farben, Wortschatz, Sprache). Die Schutz-Frage („wer sieht was?") lebt im Schwester-Konzept 🔒 Sicherheit & Datenschutz First.
§1Das Modell — Mandant · Entität · User
⛔ DREI HOHEITEN, EIN SATZ — er gilt überall:Mandant = Datenraum · Entität = Firma · Projekt = Zuschnitt. Die Probe, die sie auseinanderhält, sind drei Fragen: Wer darf es sehen? → Mandant · Wem gehört der Beleg? → Entität · In welchem Zuschnitt wird gearbeitet? → Projekt. Wo eine Antwort auf zwei Fragen passt, ist eine Achse verlorengegangen. Die volle Kaskade lautet Plattform → Mandant → Entität → Projekt → Daten/Rechte/Regeln (Projekte §1). ⛔ Wer eine kürzere Kaskade schreibt, nennt die weggelassene Hoheit und den Grund — die Kaskade in der Zeile darunter lässt das Projekt bewusst weg, weil sie die Besitz-Hierarchie zeigt und nicht den Arbeits-Zuschnitt. Eine Verkürzung, die sich nicht als solche zu erkennen gibt, ist beim Nachbau von einer vergessenen Achse nicht zu unterscheiden.
Hierarchie: Mandant (= Datenraum + Owner) → Entitäten (C3 · C4 · Privat …) → User mit Rollen. Owner ≠ Zahler ≠ Nutzer — drei getrennte Fragen (wem gehört der Raum · wer bezahlt · wer arbeitet darin). Selbstständige tragen „2 Mützen": EIN Mandant, mehrere Entitäten (Geschäft + Privat) — die Entität entscheidet nur, wer AUSSER dem Owner sieht — innerhalb des Mandanten. ⛔ Sie ist die Verengung, nicht die Wand: 05_SCHEMA/213_sicherheit01_entity_rls_membership.sql:55 löst die erlaubten Entitäten über entities.mandant_id auf — wer die Entität für den Datenraum hält, verspricht einen Schutz, den erst der Mandant trägt.
Zwei Welten, ein Mandant: Welt A = Beratung (prf_projects, KIVISAI-Arbeit AM Kunden) · Welt B = App-Nutzung (projects, der Kunde arbeitet SELBST in TGV). Beide hängen am selben Mandanten, bleiben aber getrennte Datenräume (Mig 204). Der Mandant ist die EINE Klammer.
⛔ USER ≠ MANDANT — eine Identität, viele Beziehungen (konsolidierter JC-Entscheid 04.10.2026, JE-186 bis JE-188): ein User besitzt mit einem Login und einem Profil zugleich einen Privat-Mandanten, einen Business-Mandanten, ist Mitarbeiter, Gast in fremden Projekten oder Entitäten. Vom Gast zum eigenen regulären Mandanten gibt es keinen Identitätsbruch — die Identität wird erweitert, nie ersetzt. Der kostenlose externe Gastzugang ist Produktstrategie (Gast → Nutzung → Second Brain wächst → eigener Mandant); fremde Projektdaten werden beim Upgrade nie kopiert. Firma bleibt Firma, privat bleibt privat: Privates gehört in einen eigenen Privat-Mandanten (Haushalt: persönliche Entitäten + Entität „Gemeinsam“ mit MEMORY). „Owner“ in der Hierarchie oben heißt seit 04.10. Verwalter des Datenraums, nicht Eigentümer jedes Inhalts (JE-190). IST:user_mandant_roles + user_project_roles tragen die mandantenübergreifende Identität schon; 7 Personen mit ausschließlich Projektrolle sind die heutigen Gäste; Plan-Limits, Privat Free, PERSO-KAI und die Upgrade-Strecke sind nicht gebaut (Paket 3). Wortlaut und IST/SOLL: 00_LEITDOKUMENT/KONZEPT_MANDANT_FIRST_CLASS.md §0.4 · Betreiberzugriff und streng-Zielmodell: Sicherheit §2a.
§2Topologie heute — 6 Mandanten
Mandant
Typ / Entitäten
Stand
jcbinetti (CORE)
Solo-Unternehmer, 3 Entitäten: C3 GmbH · C4 SARL · Privat — der Ernstfall des 2-Mützen-Modells ⛔ KORRIGIERT 05.09.2026 (KONZEPT#03, #1650): der Mandant jcbinetti trägt eine Entität — privat. C3 und C4 sind eigene Mandanten mit je einer Entität (gemessen 05.09. an entities ⋈ mandanten). Der Ernstfall des 2-Mützen-Modells bleibt — er läuft nur über die Mandanten-Wand, nicht innerhalb eines Mandanten, und ist damit der härtere Fall, nicht der weichere.
LIVE (Pilot)
gabriel
Typ B: eigenes Produkt (Lebenserinnerungen) mit eigener Marke (Voll-Flächen-Brand)
LIVE
kivisai
Betreiber-/Beratungs-Mandant
angelegt
anna
erster echter EXTERNER Mandant (Expat-Coaching) — der Ernstfall fürs Onboarding (§5)
wartet auf M1–M5 + Wizard
lorenzo · julien
weitere geplante Mandanten (Familie)
angelegt/geplant
Topologie-Entscheid: 6 Mandanten, uuid-basiert; C3 ≠ C4 (zwei Firmen, zwei Entitäten). Jeder Mandant hat seine feste Farbe im Farb-Kanon.
⛔⛔ EIN ZURÜCKGENOMMENER BEWEIS — UND DIE QUELLE, AUS DER ER IMMER WIEDER NEU ENTSTEHT (KONZEPT#03, 05.09.2026, #1650). Registerzeile #1628 begründete am 05.09., die Trennung der beiden Namensräume trage strukturell — Beleg: eine Seed-Zeile stelle c4-convis-sarl unter den Mandanten c3-convis, also zwei Entitäten in EINEM Datenraum. Der Satz ist zurückgenommen (in #1628 durchgestrichen, nicht gelöscht). Er stützte sich auf genau den Entscheid, den JC am 07.07.2026 widerrufen hat:„C4 ist EIGENER Mandant … C3 und C4 sind 2 Firmen → Daten HART getrennt." ⭐⭐ Die gefährliche Richtung ist nicht die falsche Zahl, sondern die falsche Entlastung: der Beweis beruhigte in einer Sicherheitsfrage — mit dem Gegenteil dessen, was gilt. Eine falsche Zahl wird angezweifelt, eine falsche Entlastung nicht. ⛔ DIE QUELLE, gefunden und markiert:05_SCHEMA/209_m1_mandanten_first_class.sql:211-214 trägt den widerrufenen Stand bis heute, und ihr Mandanten-Seed (:192-198) kennt c4-convis überhaupt nicht — der Mandant entsteht erst in Mig 210. Die Migration ist gelaufen und wird nicht geändert; sie hat seit 05.09. einen Korrektur-Vermerk im Kopf. ⚠ Wirkungsrichtung, weil ein Seed nicht gelesen, sondern AUSGEFÜHRT wird: heute fährt ihn nichts automatisch (geprüfte Regale: .github/workflows/ · 08_OPS/apply_migration.py und die übrigen 08_OPS/*migrat* · 05_SCHEMA/_applied_snapshot.json · 07_TESTS/ — alle Fundstellen zitieren die Datei, keine führt sie aus). ⛔ Bei einer frischen Einrichtung aber schließt ON CONFLICT DO NOTHING die Lücke NICHT: der Schlüssel ist UNIQUE(mandant_id, slug), also läge c4-convis-sarl danach zweimal — einmal korrekt und einmal im C3-Datenraum. Ein Neuaufbau muss den Seed vorher an Mig 210 angleichen. ⭐ Was OFFEN bleibt — eine Rücknahme beantwortet die Frage nicht: es gibt heute keine Struktur, die Mandanten- und Entitäts-Namensraum auseinanderhält. Was trägt, ist die uuid-Auflösung in my_entities (213_sicherheit01_entity_rls_membership.sql:55) plus sechs zufällig eindeutige Zeichenketten — und zwei davon sind ausdrücklich befristet (209:…„Platzhalter-Entitaet — TODO M5 verfeinern" für gabriel und anna-leonenko). Die Gegenprobe gehört nach M5 erneut gefahren.
Namens-Kanon + Switcher-Label (JC-Entscheid, Mig 233 — ⛔ STAGED, Apply = JC-DDL-GO): ZWEI sauber getrennte Dinge — ① mandanten.name trägt den kanonischen RECHTSNAMEN (C3 = „Convis Consult & Marketing GmbH"; heutiger DB-Wert ist ein Defekt und wird korrigiert) · ② der Mandanten-Switcher im Header zeigt das KURZE neue Feld short_label (Fallback: name) — der Rechtsname wird nie verkürzt. ⚠ Reihenfolge: Mig 233 MUSS applied sein, bevor die API/Client-Folge live geht. Hinweis AR-238: der Switcher selbst wandert perspektivisch ins User-Rollen-Menü (Hero trägt dann den Scope, K-11).
⛔ Vor Mandant Nr. 2 blockierend (Identitäts-Naht, MANDANT#01): DB-Fundament steht; es fehlen account-status-Echtdaten + serverseitiger Endpoint-Scope-Guard auf allen Skill-Endpoints (fail-closed) — heute validieren erst 2/57 Endpoints den Scope, und service_role bypasst RLS. Details + volle Lücken-Landkarte: Sicherheit §5.
Login (AUTH-Konzept): 3 Wege, alle self-service (Magic-Link · Passkey · Recovery), Rollen bewusst schlank — „0 Support" als Design-Ziel.
§3bDie Altlast der einen Mandanten-Zeit — gemessen 04.08.
Solange es einen Mandanten gab, war 'jcbinetti' als Voreinstellung richtig. Genau das macht diese Altlast schwer zu finden: sie ist kein Fehler, der geschrieben wurde, sondern eine Annahme, deren Gültigkeit abgelaufen ist (R-186). Ein Sweep über 05_SCHEMA/ zeigt, wie weit sie reicht:
Befund
Zahl
Fundstelle
Kanonische Achse (M2): mandant_id uuid als FK auf mandanten(id)
5 Tabellen
Mig 210 §1
Alte Achse: mandant_id/owner als text mit Identitäts-Default
7 Spalten
023 · 027 · 189 · 193 · 197 · 198 · 202
⛔ Und sie schreibt dieselbe Person dreifach: 'jc' · 'jcb' · 'jcbinetti'
1 · 2 · 4
ebenda
⛔ Das ist nicht eine Altlast, sondern zwei — und die zweite ist die teurere. ① Die Voreinstellung friert einen Mandanten ein (R-184, Register #323 zeigt dieselbe Klasse in der Namensbildung). ② Die Achse existiert doppelt und typ-unverträglich: kanonisch ist uuid mit Fremdschlüssel, real stehen daneben sieben text-Spalten — Mig 210 nennt prf_contracts selbst als Ausnahme („HAT BEREITS mandant_id text DEFAULT 'jcbinetti'"). ⇒ Dieselbe Krankheit wie die drei Entitäts-Vokabulare, eine Ebene höher: wer den Mandanten künftig hart filtert, muss zwei Typen und drei Schreibweisen versöhnen, nicht einen Wert ersetzen. Erst das Vokabular, dann der Filter — sonst filtert der Guard sauber gegen die falsche Hälfte des Bestands.
✅ BEANTWORTET 04.08. (Tower#32) — die Frage „ist intern als Voreinstellung fail-closed genug?" hat ZWEI Antworten, weil es zwei Achsen sind:
· Auf documents/rag_chunks: NEIN. Dort gilt weiter fail-closed auf die strengste Stufe — AGENT-ORCH#01 hat den Default in dieser Nacht ausdrücklich nicht angefasst.
· Auf prf_contracts: JA. Dort ist intern nach E-50① der fachlich richtige Regelfall; die Ausnahmen benennt Mig 240.
⇒ Zwei verschiedene Achsen, zwei verschiedene Defaults — das ist kein Widerspruch, sondern die Anwendung. Steht hier, damit die Frage nicht in drei Wochen erneut gestellt wird.
⛔ WIDERLEGT 04.08. — diese Spur stand hier und trägt NICHT. Sie bleibt sichtbar durchgestrichen stehen, statt still zu verschwinden: eine Tabelle, die bei einem Achsen-Umbau als Ausnahme stehen blieb, ist der Ort, an dem Policies und Grants nicht mitgezogen wurden.
Gegengemessen von AGENT-ORCH#01 (read-only), zwei Belege: ① Mig 212 (RLS-Neubau) lief am 07.07. — invoice_inference schrieb am 13.07. erfolgreich in prf_contracts; die Tabelle ist also nach der Migration beschreibbar. ② Der Skills-Service nutzt service_role — dieselbe Rolle, dieselben Header, derselbe Payload wie der Direkttest, der HTTP 201 lieferte. ⇒ Gleiche Bedingungen, unterschiedliches Ergebnis: die Rechte-Erklärung ist erledigt.
Der wahre Grund ist gefunden und er ist bitterer: der Skill hat den Fehlertext weggeworfen — protokolliert wurde nur persist_error_HTTPError, der Ausnahme-Typ, ohnecode/message/details/hint von PostgREST. Zwölf Läufe lang wusste niemand, ob 400, 401, 403 oder 500 — und daraus stammt die ganze Rate-Kette, diese Spur eingeschlossen. Regel dazu: R-188.
⚠️ Was hiervon NICHT betroffen ist: die Achsen-Messung oben (7 text-Spalten · 3 Schreibweisen · 5 uuid-Tabellen · prf_contracts als Mig-210-Ausnahme) bleibt vollständig gültig. Sie erklärt nur diesen Write-Fehler nicht. Ein Befund verliert nicht seinen Wert, weil er eine bestimmte Frage nicht beantwortet — er verliert ihn, wenn man ihn als Antwort ausgibt, die er nie war.
Nicht mitgezählt, damit die Regel nicht alles einsammelt: Die Deploy-Skripte mit REPO = "jcbinetti/tgv-dashboard" sind kein Fall dieser Klasse: dort ist der Eigenname korrekt, weil er ein reales GitHub-Konto bezeichnet, keine Mandanten-Annahme.
§4White-Label — jedem Mandanten SEIN Produkt
Achse
Wie es funktioniert
Anker
Stand
Farben (Marke)
data-brand am Root: zwei Override-Stufen — reine Akzent-Marke (megabook-Muster) oder Voll-Flächen-Marke (gabriel-Muster); Bedeutungs-Anker confirm/warn/danger bleiben IMMER gleich
LLM-Provider-Governance per Mandant + Skill konfigurierbar (Ziel; heute hardcoded) + Cost-Telemetrie
project_llm_provider_governance
Ziel
§5Onboarding — der Anna-Ernstfall
Der erste externe Mandant (Anna, Expat-Coaching) definiert die Latte: Onboarding als geführter KAI-Wizard statt Formular-Wüste — Mandant anlegen · Entitäten · Rollen · Marke (Farben/Anrede/Sprache) · Datenquellen · Verwaltet-Modus als Default-Start. Voraussetzungen: M1–M5 + die Identitäts-Naht (§3). Der Betriebsmodus (Verwaltet ↔ Souverän) mit Haftungs-Konsequenz ist ein Schutz-Thema und lebt in Sicherheit §6; hier gilt: Default-Start = Verwaltet, Wechsel erst nach Awareness-Gate. Trägt der Mandant nach aussen ein öffentliches Gesicht (Kunden-Website mit Anmeldung/Zahlung/Rechnung), gilt zusätzlich §7 — Website-Factory & Typ-B-Datenhaltung.
§5.1Erster Start — eine Seite, ein Gespräch
JC 11.10.2026 (sinngemäß): Am Anfang sieht ein neuer Mandant nur ganz wenige Menüpunkte und eine einfache Seite, auf der KAI ein Gespräch beginnt. Das soll sehr einfach sein und sich an ChatGPT und Claude orientieren. KAI erfasst dabei die Lage des Mandanten. Bahn ONBOARDING#01, Register #4065. Die Bereitschaftsmessung für Annas Konto steht in 01_ITERATIONS/CONTROL_TOWER_HANDOFF/BEFUND_ONBOARDING01_BEREITSCHAFT_2026-10-11.md.
Der Kern:Wer neu ist, bekommt ein Gespräch statt eines Dashboards. Ein Mandant im Zustand mandanten.status = 'onboarding' startet in der schmalen Shell, die es schon gibt (core_shell.html:5038 IA-Preset „min", Start-Route /chat). KAI führt durch vier Etappen. Jede Etappe endet mit einer Karte (Vorschlag → Bestätigen, Chatbot First §7d). Erst eine bestätigte Karte schreibt etwas, und dann erscheint der passende Menüpunkt. Neu gebaut wird keine Tabelle: Das Wachstum ist mandant_modules (Mig 219, so beschlossen in Projekte §6).
Etappe
KAI fragt (Inhalt frei)
Karte schreibt (Form fest)
Danach sichtbar
Baustein
0 · Zugang
Diese Etappe ist kein Gespräch. Vor dem ersten Satz steht der Einwilligungs-Bildschirm.
„Erzähl mir von dir und deiner Arbeit: Wer gehört dazu, was soll ich über dich wissen?“
Steckbrief: jede Angabe einzeln mit stimmt · ändern · verwerfen. Eine leere Antwort ist nie ein Ja.
„Profil“ im Nutzermenü
persona_wizard extract/steckbrief, persona_profiles (Mig 310) CLI, nicht in KAI
2 · Entität
„Unter welchem Namen arbeitest du, in welchem Land, als was?“
Name, Land und Rechtsform der vorhandenen Entität
Entität im Kopf
entitaet-post-setzen (app.py:9320) kein KAI-Werkzeug
3 · Eingang
„Was kommt bei dir an: Rechnungen, Verträge, Kontoauszüge?“
passende Module einschalten
„Dokumente“, „Konten“ …
mandant-modul-schalten (app.py:9091) kein KAI-Werkzeug
4 · Fertig
„Lad dein erstes Dokument hoch, ich ordne es ein.“
Zustand onboarding → active
die volle, aber nur gebuchte Navigation
set-mandant-status (app.py:9885) gebaut
Form fest, Inhalt frei. Die Etappen und ihre Felder liegen fest, also das, was eine Karte schreiben darf. Wie KAI fragt, nachhakt und erklärt, ist frei; Antwort-Vorlagen gibt es nicht. KAI liefert die Absicht (Kartentyp und Felder), und der Renderer baut daraus die TGV-Karte. Wer eine Etappe überspringt, kann sie später im Gespräch nachholen („Richte mir noch die Konten ein“). Der Assistent ist kein Tunnel, er kommt immer wieder (WIZARD_01_PERSONA_WIZARD/KONZEPT.md:27-36, ④ wiederkehrend).
Wenige Menüpunkte, die wachsen — wie es heute schon fast geht:
① Das schmale Gerüst existiert, schaltet sich aber nur über den Adress-Zusatz ?ia=min ein (core_shell.html:5045). Neu wäre, dass es sich nach dem Mandanten-Zustand richtet statt nach der URL.
② Das Menü-Sieb existiert (navItem → _modulSperreFuer, core_shell.html:11368-11400). Heute gilt: Fehlt eine Zeile, ist das Modul AN, und Annas Mandant hat keine Zeile. Ein frischer Mandant bekommt deshalb beim Anlegen seine Modul-Zeilen auf aus (Default-Deny; Mig 219:66 sieht genau das vor: „beim Onboarding-Wizard“). KAI schaltet sie per Karte ein.
③ Die Klassen-Wand bleibt unberührt (klasseErlaubt, core_shell.html:4699). Sichtbar wird höchstens, was die Rolle ohnehin darf.
⛔ Sicherheit — der Anna-Fall hat zwei Rollen, und sie dürfen nie verschmelzen. Anna ist dieselbe Person in zwei Beziehungen. Belegt ist das in drei Quellen: md_persons „anna“ mit Haushaltsbezug (Register #3789), die Karte „Anna Leonenko … keine TGV-Entität“ (05_SCHEMA/145:58) und der Mandant anna-leonenko (05_SCHEMA/209:196). Daraus folgen ein Konto und zwei Wege:
ⓐ Mitglied (mandant_admin) nur in anna-leonenko. Damit sieht sie ihre eigenen Daten.
ⓑ In JCs Mandant ist sie kein Mitglied. Sie sieht dort nur, was JC ihr einzeln freigibt (Einzelfreigabe, JC-Entscheid V03 ② vom 11.10., #4046).
Eine Mitgliedschaft in jcbinetti, auch eine verengte nach 08_OPS/sicherheit03_seed_anna.sql, wäre ein dritter Weg und würde ⓑ unterlaufen. Der Scope kommt aus der Mitgliedschaft (scope_gate.py:150-160), nie aus dem Deskriptor. Deshalb braucht der Erststart einen Gegenbeweis: Annas Konto sieht 0 Belege außerhalb von anna-leonenko und ihrer Freigaben, und JCs Viewer sehen 0 von Annas Belegen.
Zwei Varianten — Vorschau
Ⓐ Empfehlung · Gespräch (wie ChatGPT/Claude)
KAIDokumente+ KontenProfil
1 Lage2 Entität3 Eingang4 Fertig
Hallo Anna, ich bin KAI. Bevor ich dir helfe, möchte ich dich kennenlernen. Erzähl mir von deiner Arbeit.
Ich coache Expats in Frankreich, selbständig, Rechnungen schreibe ich selbst …
Das habe ich verstanden Tätigkeit: Expat-Coaching · Land: Frankreich · Form: selbständig StimmtÄndern
Was kommt bei dir an? Ich schalte dir dafür Konten frei.
Schreib KAI …
Ⓑ Schritt-Assistent (Formular + KAI am Rand)
1 Profil2 Entität3 Eingang4 Fertig
Deine Entität
Name · Anna Leonenko · Expat-Coaching
Land · Frankreich
Rechtsform · auswählen …
ZurückWeiter
KAI Bei Selbständigen in Frankreich ist das meist „micro-entrepreneur“. Soll ich das eintragen?
Empfehlung Ⓐ. Sie entspricht JCs Bild wörtlich (eine Seite, KAI beginnt), dem Prinzip Chatbot First (die Oberfläche ist dünn) und der Shell, die schon gebaut ist: Route /chat, IA-Preset „min“, Karten mit Vollzug. Ⓑ ist schneller zu bauen und lässt sich leichter prüfen, ist aber ein Formular mit Chat-Beiwerk und genau die „Formular-Wüste“, die §5 ausschließt. Ⓐ übernimmt von Ⓑ den einen Vorzug: Die vier Etappen zeigt eine Fortschrittszeile über dem Gespräch an, damit niemand fragen muss, wie lange das noch dauert.
Bau-Etappe für Ⓐ
Wiederverwendet
Neu (als Baustein)
E1 Shell
IA-Preset „min“, secChat, module_aus aus account-status
Das Preset schaltet sich bei status='onboarding' ein statt bei ?ia=min, dazu eine Fortschrittszeile.
Die drei Werkzeuge und dazu „Modul einschalten“ und „Entität benennen“ werden in den tool_scope einer Onboarding-Domäne und in das VOLLZUG_REGISTER aufgenommen.
E3 Zeremonie
create-mandant (app.py:8478)
legt die Entität, die Modul-Zeilen aus und den ersten mandant_admin an (heute nur die Mandanten-Zeile)
E4 Beweis
Mandanten-Wand-Proben
Gegenbeweis für einen frischen Mandanten in beide Richtungen, als Probe mit Parametern
✅ JC-Entscheid 11.10.2026 (über TOWER#72, Register #4090): eine KOMBINATION aus Ⓐ und Ⓑ. Anna hat einen Mandanten-Zugang gekauft oder einen Monat Probezugang. Sie meldet sich per Magic-Link an und sieht ein minimales Menü und einen prominenten KAI mit drei vorgefertigten Fragen, darunter „Erkläre mir, was TGV kann und wie es funktioniert“ und „Lass uns mein TGV in einem netten Dialog konfigurieren“. Dann führt KAI: Wer bist du, was machst du, wie soll ich mit dir sprechen, welche Bereiche zuerst (Second Brain, Finanzen, Buchhaltung, Steuererklärung, Website, Projekte, Reisekosten, Verträge …). Es gibt offene und geschlossene Fragen, Antworten per Diktat oder Eingabe. Alle Antworten und ihr Kontext werden gespeichert. Daraus wächst das Menü, und jeder Menüpunkt folgt einer Intention. Danach kommen Upload und Eingang, wie heute (Kaskade, Monitoring).
⇒ Von Ⓐ bleibt das Gespräch als Hauptweg, von Ⓑ die Fortschrittszeile und die geschlossenen Fragen als Auswahl-Chips. Rechtstexte: für den Testbetrieb noch nicht zwingend (JC). JC bleibt super_admin in Annas Mandant (Debugging-Phase), deshalb wird Mig 215 nicht gefahren.
Bau-Schnitt
Was
Wiederverwendet
Hängt an
S1 Erster-Start-Seite
gebaut #4104, Deploy = JC-Wort Sind ALLE Mandanten der angemeldeten Person im Zustand onboarding und ist sie nicht super_admin, startet die Shell auf KAI. Das Menü wird aus einer festen Liste gebaut (KAI fragen · Dokumente), nicht aus dem Voll-Menü gefiltert, und die Modul-Sperre greift weiter. JCs drei Einstiege stehen im leeren Gespräch über dem Composer, damit sie auch am Telefon sichtbar sind (dort startet der Kopf nach #689 zugeklappt). Die Fortschrittszeile kommt mit S2, denn erst dort gibt es Fortschritt, den man messen kann.
secChat, Leer-Zustand syncKaiEmpty, navItem (Klasse + Modul), Diktat im Composer (live). ⛔ ?ia=min NICHT: Das Preset zeigt inzwischen das ganze Kanon-Menü mit Staging.
Shell-Deploy = JC-Wort
S2 Konfigurations-Dialog
Eine KAI-Instanz „Einrichtung“: Wer, was, Ton, Bereiche. Jede Angabe läuft über eine Karte (stimmt · ändern · verwerfen). Gespeichert werden alle Antworten und ihr Kontext.
#3495 (Menü-Kanon) · Lücke: „Website“ und „Projekte“ haben keinen Modul-Schlüssel — Vorschlag Bereich ≠ Modul in §6.1 (MODUL#02 #4107)
S5 Anmelde- und Einladungs-Mail aus TGV
JC 11.10.: „Die Mail-Nachricht müssen wir verwalten können.“ Heute verschickt Supabase sie mit einer projektweiten Vorlage, womöglich der Gabriel-FR-Vorlage. Neu: TGV erzeugt serverseitig Link und 6-stelligen Code (generate_link) und verschickt beides über die EINE Versandpipeline (mail_buchfuehrung.senden_gebucht, Klasse 3), mit einer Vorlage je Mandant und Sprache aus mail_templates. Dazu: eine Landeseite mit Knopf (ein Link-Prüfer verbraucht den Link dann nicht) und mit Code-Eingabe (verifyOtp; einen Code kann Brevo nicht umschreiben, MAIL_VERSAND X5a Weg B, JE-181) sowie der Editor unter Konto-Verwaltung → Mail-Vorlagen. Supabase bleibt der Rückfall.
DDL (JC-Wort): Primärschlüssel use_case → (use_case, mandant_id, locale), RLS-Vorlage PROPOSED_281 · Token nie in outbound_mails (Inhalt schwärzen) · Rate-Limit, nur bestehende Konten
S4 später
Zeremonie create-mandant (Entität, Module aus, erster Verwalter) für Kundin Nr. 2 · eigene Screens je Intention (Chatbot First §5)
app.py:8478
—
§6Module & Lizenz — was ein Mandant gebucht hat
M6 (Modul-/Lizenz-Layer): welcher Mandant welche Module nutzt — sichtbar und buchbar („Module & Erweiterungen" als Store, JE-009 offen). Der Weg dorthin: Modul-Katalog M0–M8 — eine Lane wird zum Modul befördert, wenn sie 4 Schwellen erfüllt (Promotions-Gate). So wächst das Angebot aus bewiesener Arbeit, nie aus Versprechen.
§6.1Modul · Paket · Bereich — ein Katalog für Preis, Menü, Einstellung, KAI und Einrichtung
JC 11.10.2026 (sinngemäß): Im Einrichtungs-Dialog nennt ein Nutzer Bereiche, die er zuerst will (Second Brain, Finanzen, Buchhaltung, Steuererklärung, Website, Projekte, Reisekosten, Verträge …). Unter Verwaltung → Module stehen aber schon Module (dokumente · finanzen M2 · kontakte · mail M1 · megabook M5 · persona · recherche · rka M3 · steuer M2 · system · vertrag M2, alle „Standard (keine Zeile)“). Wie definieren wir Module sinnvoll? Das trifft Pricing, Geschäft, Einstellung, Kundenerwartung und UX. Dazu kommt aus EXPAT#01 (JCs Expats-Brief vom 11.10., Teil B): Preise und Produkte aus EINER Quelle. Bahn MODUL#02, Register #4107, gemessen an origin/master 008820083.
Der Kern:Ein Bereich ist nicht dasselbe wie ein Modul. Von JCs acht Bereichen landen drei auf derselben Fähigkeit finanzen und vier auf dokumente; zwei (Website, Projekte) haben gar keinen Schlüssel. Wer Bereich, Fähigkeit und Preis in EIN Wort presst, muss entweder Werkzeuge doppelt zuordnen oder dem Nutzer technische Schnitte zeigen. Empfehlung: drei Ebenen in EINEM Katalog: Fähigkeit (technisch, gibt es) · Paket (verkaufbar, mit Preis) · Bereich (Nutzerziel, schaltet frei). Alle Leser lesen dieselbe Datei.
✅ JC-Entscheid 11.10.2026 (über TOWER#72, weitergegeben von TOWER#73, Register #4131): ① Variante Ⓑ: ja. ② Neue Fähigkeiten website und projekte: ja. ③ „Gebucht“ schreibt nur Betreiber/Kasse — noch nicht entscheidbar. ④ Pakete und Preise — noch nicht. ⑤ M-Codes aus der Oberfläche — nein, noch nicht.
JC sinngemäß: „Das Konzept muss mit Vertrieb und Reifegrad der Module weiter reifen.“ ⇒ Gebaut werden nur ① und ②, also die Ebenen Fähigkeit und Bereich (Bau-Schnitt K1 ohne Pakete). ③–⑤ stehen als offene Punkte mit Auslöser im Register (#4132), siehe Tabelle unten. Bis ③ entschieden ist, bleibt ein Modul ein Schalter des Mandanten, und Default AN gilt unverändert.
Was „Standard (keine Zeile)“ bedeutet: In mandant_modules steht für diesen Mandanten keine Zeile. Fehlt die Zeile, ist das Modul AN (modul_kanon.json → _default_ist_AN, Mig 219:40). Abgeschaltet ist also nichts. Was „M1/M2/M3/M5“ bedeutet: Das ist das Attribut katalog und verweist auf den Tower-Vorschlag „Modul-Katalog M0–M8“ (00_LEITDOKUMENT/WORKSHOP_MULTI_MANDANTEN_KONZEPT.md:196, §17): M1 Eingang · M2 Finanzen (CORE) · M3 Reisekosten · M5 Lebenswerk/Memoir. Fünf der elf Schlüssel haben keinen Code (dokumente, kontakte, recherche, persona, system), drei teilen sich M2 (finanzen, vertrag, steuer). ⛔ Dieselben Buchstaben bedeuten auf dieser Seite noch etwas anderes: in §3 ist M1–M6 der Migrations-Fahrplan (dort ist M6 der Lizenz-Layer). Für einen Kunden sagt „M2“ also nichts, und für uns sagt es zweierlei.
§6.1aWer liest den Kanon heute — gemessen
Leser
Liest
Fundstelle
Stand
Schlüssel
11 module_key mit Bedeutung, Altschlüssel documents/costs, Default AN
10 Bildschirme tragen modul (dokumente · buchungen · konten · vertraege · kontakte · fahrtenbuch · abgleich · monatsabschluss · buchhalter · reisekosten). Ist das Modul in jedem Mandanten des Aufrufers aus, verschwinden sie und erklären sich.
49 Menü-Einträge mit gruppe · eintrag · pfad · route · klasse · bauform, aber ohne modul. KAI könnte also einen Weg nennen, der für diesen Mandanten abgeschaltet ist.
chat_engine/menue_kanon.json auf PR #3495 (KAI-QA#03 #4087)
offen, Lücke
Einrichtung
persona_wizard kennt drei Ziele (steuer_dossier · fahrtenbuch · memoir), ohne Bezug zu Modulen
persona_wizard/followup.py:30
fünftes Vokabular
Preis · Rechnung
kein Leser.mandanten.plan existiert als „Billing-Anker“; in app.py und core_shell.html kein Leser gefunden. Kein Preis im Haus. Expats rechnet produktiv (Baustein preis-hoheit, Grad F2).
Der Verwalter eines Mandanten darf jede Modul-Zeile seines Mandanten schreiben (RLS mandant_modules_write FOR ALL; Endpunkt prüft _darf_regel_setzen). Ein Modul ist heute ein Schalter des Mandanten, keine Lizenz.
05_SCHEMA/219:56-60 · app.py:9188
vor Pricing klären
§6.1bJCs Bereiche gegen die vorhandenen Fähigkeiten
Bereich (Nutzerwort)
braucht die Fähigkeiten
Einstieg
Lücke
Second Brain
dokumente · recherche · persona
KAI fragen · Dokumente & Ablage
—
Finanzen
finanzen · kontakte · dokumente
Buchungen · Konten & Karten
—
Buchhaltung
finanzen · dokumente
Monatsabschluss · Buchhalter
gleiche Fähigkeit wie „Finanzen“, anderes Ziel
Steuererklärung
steuer · finanzen · dokumente
Bereich Steuer
Profil-Ziel steuer_dossier existiert, ist nicht verbunden
Website bauen
neu: website
—
bis K1 kein Schlüssel; seit K1 (#4138) Schlüssel website, website-weg hängt daran
Projekte durchführen
neu: projekte
Projekte
bis K1 kein Schlüssel; seit K1 (#4138) Schlüssel projekte (M4), Bildschirm /projekte trägt ihn
Reisekosten
rka
Reisekosten · Fahrtenbuch
Profil-Ziel fahrtenbuch nicht verbunden
Verträge optimieren
vertrag · dokumente
Verträge
—
Lebensgeschichte
megabook
(eigene App, megabook.fr)
Profil-Ziel memoir nicht verbunden
Gelesen: Neun Bereiche verteilen sich auf elf Fähigkeiten, und die Zuordnung ist viele-zu-viele. Ein Bereich ist eine Absicht (wofür ich TGV will), eine Fähigkeit ist ein Werkzeugbündel (was TGV dafür tut). Ein Bereich, der weggeschaltet wird, darf keine Fähigkeit mitreißen, die ein anderer gewählter Bereich braucht.
§6.1cZwei Varianten — Vorschau
Ⓐ Ein Wort für alles — das Modul ist Fähigkeit, Produkt und Bereich
Wirksam ist, was das Paket erlaubt UND der Mandant eingeschaltet hat. Der Mandanten-Verwalter schaltet innerhalb seines Pakets frei (heutiger Schalter, bleibt). Was über das Paket hinausgeht, zeigt die Verwaltung als „buchbar“ statt als Schalter. Bestandsmandanten bekommen das Haus-Paket „alles“: an ihrem Verhalten ändert sich nichts, und Default AN bleibt für sie gültig.
⛔ Die Lesepflicht für den Preis (aus EXPAT#01, JC 11.10.): In Expats musste die Preisänderung vom 11.10. an sieben Stellen nachgezogen werden: Seite · Schema-Daten · Anmeldeformular · register.php · promo.php · Kostja-Prompt · Assistent. Quelle: JCs Expats-Brief §B (expatscoaching-app/docs/BRIEF_TGV_2026-10-11.md; Archiv-Kopie legt EXPAT#01 im Workstream-Ordner WS_AUSSENSEITE_COMMERCE ab, PR #3519), Übergabe von EXPAT#01 am 11.10. In TGV gilt deshalb vom ersten Preis an: Seite, Formular, Server-Preisrechnung, Zahlung, Rechnung und KI-Assistent lesen den Preis aus dem Katalog. Die KI ist die Leserin, die man am leichtesten vergisst. KAI nennt einen Preis nur aus dem Katalog, genau wie ein Menü-Weg nur aus dem Menü-Kanon kommt. Der Server rechnet, dem Browser wird kein Preis geglaubt (Baustein preis-hoheit, TGV_PLUG_AND_PLAY.md:148; Promocode KONZEPT_WEBSITE_FACTORY.md:140).
Empfehlung: Ⓑ, als Erweiterung der vorhandenen Datei.08_OPS/modul_kanon.json bekommt neben module die Abschnitte pakete und bereiche. Es gibt keine zweite Datei und keinen neuen Leser-Weg: Dienst (_modul_kanon), Bündel-Zeile und Wächter (check_modul_kanon.py) sind schon da. Der Wächter prüft zusätzlich: jede Fähigkeit eines Bereichs und eines Pakets ist ein Kanon-Schlüssel, und jeder Bereich ist in mindestens einem Paket ganz enthalten. Warum nicht Ⓐ: Ⓐ ist auf den ersten Blick einfacher, aber „Buchhaltung“ und „Finanzen“ müssten zwei Schlüssel für dieselben Werkzeuge werden, und jede technische Teilung würde die Preisliste ändern.
OK-Frage an JC
Empfehlung
Art
JC 11.10.
① Variante
Ⓑ drei Ebenen, ein Katalog in modul_kanon.json
Architektur
ja
② Zwei neue Fähigkeiten
website und projekte; website-weg wandert von system zu website
Architektur
ja
③ Lizenz vs. Schalter
„Gebucht“ schreibt nur Betreiber oder Kasse, der Verwalter schaltet nur innerhalb. Folge: eine neue Migration engt die Schreib-Policy von Mig 219 ein (DDL = JC-Wort)
Geschäft + Sicherheit (hartes Tor)
offen — Auslöser: der erste Mandant, der für ein Modul zahlt (vorher kauft niemand etwas, das er sich selbst einschalten könnte)
④ Paket-Zuschnitt
Struktur: Grundpaket (KAI, Dokumente, Kontakte, Profil, Mail) + Bereichs-Pakete + Probemonat. Namen und Preise setzt JC.
Geschäft
offen — Auslöser: Vertrieb (erstes Angebot an einen Kunden) und Reifegrad (ein Modul erfüllt das Promotions-Gate aus §6)
⑤ Anzeige
Die M-Codes verschwinden aus der Oberfläche; Verwaltung → Module zeigt Bereich, Paket und Zustand
UX
nein, noch nicht — die M-Codes bleiben sichtbar; Auslöser zum Wiedervorlegen: ④ ist entschieden (dann gibt es Paketnamen, die sie ersetzen können)
Bau-Schnitt (Stand nach JCs Wort 11.10.)
Was
Wiederverwendet
Hängt an
K1 Katalog
bereiche in modul_kanon.json, Schlüssel website/projekte, Wächter erweitert. Ohne pakete (④ offen).
check_modul_kanon.py, _modul_kanon
① ② gebaut MODUL#02 K1 #4138, Dienst 1.12.452 (Deploy = JC-Wort)
K2 KAI-Wege
Der Menü-Kanon trägt modul aus SCREEN_KANON; KAI nennt nur Wege, die für den Aufrufer an sind
§5.1 S3 liest bereiche: Chips = Bereiche, Karte schaltet deren Fähigkeiten; Profil-Ziele werden Bereiche
mandant-modul-schalten, persona_wizard
K1 · #3472
K4 Lizenz
Paket als Obergrenze im Dienst und in der RLS
mandanten.plan (Mig 209:99)
③ offen · DDL = JC-Wort
K5 Preis-Leser
Seite · Formular · Rechnung · KAI lesen pakete
preis-hoheit, Promocode-Engine
④ offen · erster zahlender Mandant
§7Das öffentliche Gesicht — Website-Factory & die Typ-B-Datenhaltung
Ein Mandant kann nach aussen ein öffentliches Gesicht tragen: eine Kunden-Website mit Anmeldung, Zahlung, Rechnung und geschütztem Bereich (Annas Expat-Coaching ist der Live-Fall). Der Leitgedanke ordnet dieses Gesicht in TGV ein — und die Website-Factory ist die wiederverwendbare Fähigkeit, es zu bauen, statt es je Kunde neu zu erfinden. SSOT dieses Kapitels: 00_LEITDOKUMENT/KONZEPT_WEBSITE_FACTORY.md (Ernte-Routine §4.4 · Prüfstein ERNTE#01 §4.5 · Adapter-Architektur §10) — hier wird eingefaltet, nicht gedoppelt.
Der Leitgedanke (Tower#32):Annas Website ist keine Website — sie ist ein TGV-Mandant mit einem öffentlichen Gesicht. Jedes Ding auf der Seite existiert in TGV bereits als Begriff. Wir haben TGV zweimal gebaut — einmal davon in PHP. Deshalb wird Anna nicht migriert, sondern zuletzt und freiwillig: erst der ZWEITE Kunde beweist einen Kontrakt, beim ersten weiss man nie, was Zufall war.
Nachtrag EXPAT#01 (11.10., Register #4106): JCs Brief mit 13 Bausteinen aus dem Live-Betrieb ist in KONZEPT_WEBSITE_FACTORY.md §4.2b je Baustein gegen TGV gemessen. Den Brief selbst führt das Archiv: 01_ITERATIONS/WS_AUSSENSEITE_COMMERCE/BRIEF_EXPATS_TGV_2026-10-11.md.
Zwei Bausteine hat TGV schon in eigener Form: den Feld-Setzer als datenkorrektur_fahren.py und den Schalter als Datenbank-Zeile statt Datei.
Die Folgearbeit im Register: Rechnungsnummer atomar im Dienst #4114 (Steuerprüfung je Mandant ist JCs Gate), Herkunft je Bild als Metadatum #4115.
Preise aus EINER Quelle (Teil B des Briefs) gehört zu §6, Bahn MODUL#02 (#4107).
⭐ Annas Magic-Link löst live seit einem Monat genau den offenen Klärungsauftrag „ein Mensch ohne TGV-Konto sieht genau seine eigenen Dokumente — nie fremde" (Website-Factory §6.5), mit echtem Geld, echtem DSGVO-Rahmen, kleinem Schadensradius. Der Adapter-Gedanke (§10): EIN Kontrakt (SSOT) + geteilte UI/UX, dünne Adapter je Ziel — Typ B = flaches PHP (Anna = Referenz-Adapter), TGV = Supabase/RLS + vorhandene Skills. YAGNI: Kontrakt + Typ-B-Adapter zuerst, TGV-Adapter erst bei echtem Bedarf.
§7.1Ist-Stand der Typ-B-Datenhaltung — gemessen, nicht behauptet
TYPB#01 hat read-only gemessen (2026-08-10, gegen die Repo-Migrationen + PROJEKT#03s Live-Befund), ob die Typ-B-Datenhaltung durchgängig an der Mandanten-Wand hängt. Kurzbefund: das Fundament steht, aber die Wand erreicht Welt B heute nicht — an drei Stellen bricht die Achse.
Achse
Gemessener Stand
Bruch?
Welt B — Struktur
public.projects trägt seit Mig 204 die Typ-B-Felder world_subtype (b1 mandantfähig / b2 extern) · stack · multi_tenant · prf_project_id (Dual-Link → Welt A). SmartFair=id5 ist reserved_domain (kein Produkt), KIVISAI=id1 (b2/react), expatscoaching=Stub (b2/stack=null/planned).
Felder da, aber dormant: nur Mig 204 schreibt sie, KEIN Leser (Code + alle Migs) liest world_subtype/multi_tenant/prf_project_id
Welt B ⇄ Mandanten-Wand
Live gemessen (DATEN#01 · PROJEKT#03):public.projects trägt KEINE mandant_id — trennt heute nach Entität; nur „Live Memory" brückt via prf_project_id nach Welt A. Der Wand-Ausbau ist geplant, aber STAGED (DO-NOT-APPLY): Mig 210 (M2) würde projects.mandant_id ohne Backfill ergänzen, Mig 212 (M4) projects super_admin-only halten. M5 (Welt-Entwirrung) hat keine Migration. (Mig-Header ≠ Live — die staged-Fassung ist nicht der Beweis, was live steht.)
JA — Welt-B-Zeilen hängen NICHT am Mandanten (live keine mandant_id-Spalte; der geplante Ausbau ist nicht appliziert). Die Wand steht für Welt A, die Welt-B-Tür ist nicht geschnitten.
Einziger Live-FK in Welt B
documents.project_id → public.projects (Welt B), 58 Zeilen, alle =5. Werte tragen aber Welt-A-Semantik (Gabriel), alle RLS-Leser lösen gegen prf_projects (Welt A) auf.
JA — Achsen-Kollision. Owner: PROJEKT#03 (Empf. FK umhängen projects→prf_projects, JC-DDL). TYPB#01 misst NICHT doppelt.
Öffentliches Gesicht (Anna)
Der Live-Fall Anna lebt ausserhalb TGV: Teilnehmer/Zahlungen/Rechnungen/Kursmaterial in PHP-Flat-Files (CSV/JSON in ../kostja_secret/ auf Strato). In TGV existiert nur die Stub-Zeile expatscoaching.
JA — das ist die „zwei Gesichter"-Lücke: konzeptionell ein TGV-Mandant, konkret eine parallele PHP-Welt. Datenhaltung heute NICHT durchgängig.
Zusammengeführter Befund: Teil ① (Konzept) und Teil ② (Messung) treffen sich an derselben Stelle — „TGV zweimal gebaut, einmal in PHP" ist keine Formulierung, sondern der gemessene Ist-Zustand. Die Typ-B-Struktur existiert im Schema, ist aber inert; die Mandanten-Wand endet vor Welt B; das eine externe Gesicht liegt komplett ausserhalb. Kein Bau in dieser Runde (JC-Rahmen „erstmal nur Konzept") — was zu tun wäre, geht als Entscheidungsvorlage an den Tower. Reihenfolge bleibt: erst der zweite Kunde beweist den Kontrakt, Anna wird zuletzt und freiwillig geführt. Die Wurzel — Welt-B-Zuordnung (M5) + Achsen-Entscheid (PROJEKT#03) — ist JC-DDL-gegatet.
§8Ein Dokument, zwei Mandanten — zwei Rollen, zwei Sichtbarkeiten
JC-Freigabe 27.08. („ok mache es gleich"), Anlassfall #8014/#8016. JCs Anforderung, sein Satz: „Ein Eingestellter, der sicher keinen Zugang zu den C3-Personalunterlagen hat, muss eine für ihn gerichtete Unterlage in seinem eigenen privaten TGV sehen können." — und seine Verallgemeinerung ist die schärfere: „eine Rechnung von C4 an C3 … das werden doch immer 2 identische Dokumente sein."
Das Modell — ein Dokument, aber NICHT eine Zeile: ein Inhalt · zwei Mandanten · zwei Rollen · zwei Sichtbarkeiten. Bei C4→C3 ist es dasselbe PDF mit entgegengesetztem Vorzeichen: Ausgangsrechnung (Forderung) bei C4, Eingangsrechnung (Verbindlichkeit) bei C3. Wer eine als Dublette tilgt, nimmt einer Seite ihre Buchung. ⛔ Die Wand bleibt unangetastet: entity wandert beim Intake, NIE in einem Nachtrag. Zwei Ausfertigungen heissen zwei Zeilen mit je eigener entity — nicht eine Zeile, die über die Wand schielt.
⭐⭐ DAS VERHALTEN IST GEBAUT — es wird nur nicht erreicht.02_SKILLS/tgv-skill-detect-duplicate/skill.py kennt den Fall bereits: entity_split = target.entity und cand.entity und target.entity != cand.entity → decision = "duplicate_entity_split". Und der Router macht daraus etwas anderes als den Tod — dispatch.py:1174: duplicate_entity_split → final_state = "needs_review", während high_confidence_duplicate → "duplicate". ⇒ „Derselbe Beleg, andere Entität" ist längst eine EIGENE Entscheidung, und sie legt den Beleg einem Menschen vor, statt ihn zu tilgen.Dieses Konzept beschreibt deshalb keinen Neubau, sondern eine REIHENFOLGE.
Stelle
Wann
Worüber sie urteilt
Das Tor — intake_raw._doc_lookup_by_sha
VOR dem Anlegen der Zeile
Über nackte Bytes: …&raw_sha256=eq.<sha>&limit=1. ⛔ Es liest entity nicht einmal — sein select führt id + NACHTRAG_FELDER (user_annotation · project_id · project_code · doc_type). Die Mandanten-Achse ist nicht ungefiltert, sie ist ABWESEND.
Der Skill — detect-duplicate
NACH dem Anlegen
Über Inhalt UND Entität — und trifft die richtige Unterscheidung. kann entity_split
⛔⛔ Wer am Tor stirbt, kommt beim Urteil nie an. Das ist die ganze Sache. ⭐ Und der Durchgriff selbst ist NICHT der Fehler: das Tor liest über _supa_rest, dessen eigene Doku „RLS bypassed — authoritative" sagt. Die Wand ist hier nicht durchbrochen — sie wird schlicht nicht gefragt, und das MUSS so sein, sonst fände das Tor gar keine Dubletten. Der Fehler ist, dass diese Stelle ENTSCHEIDET, statt nur zu FINDEN. Finden ohne Scope ist richtig; urteilen ohne Scope ist es nicht. ⛔ Dazu limit=1: es gewinnt irgendeine Zeile, nicht die richtige.
⛔ Der Schaden ist nicht „endgültig" — er ist UNSICHTBAR, und deshalb nicht zählbar. Es entsteht keine zweite Zeile (der partielle UNIQUE uq_documents_raw_sha256 … WHERE (raw_sha256 IS NOT NULL) nennt weder entity noch mandant_id), und es entsteht kein Ereignis — geloggt wird nur der Misserfolg. ⇒ Die Frage „wie oft ist das passiert?" ist per Bauart immer 0, und diese 0 bedeutet nichts.Eine Bedingung, die niemand erfüllen KANN, sieht im Bestand aus wie eine, die nur niemand erfüllt hat. ⛔ Was überlebt, sind die Bytes: save_raw läuft VOR der Prüfung — das Objekt liegt im Bucket, auf das keine Zeile mehr zeigt.
⛔ Und der Mandant der zusammengelegten Zeile ist der des ERSTEN.NACHTRAG_FELDER trägt Notiz, Projekt und Belegart nach — entity und mandant_id ausdrücklich NIE (und das ist Absicht: am Eingang ist es Erstzuweisung, im Nachtrag wäre es Verschiebung über die Wand). ⇒ Genau deshalb führt JCs „Datei ansehen" in einen fremden Mandanten — nicht weil die Wand versagt, sondern weil die überlebende Zeile nie seine war.
⛔⛔ KORREKTUR AM ANLASSFALL — gemessen, nicht erzählt: übergeben wurde mir, #8014 liege bei C3 und #8016 als Kopie bei JC privat. Beide Zeilen tragen dieselbe Entität c3-convis-gmbh; #8016 ist in JCs privatem TGV nie angekommen und wurde von DRIVE#01 content_fingerprint getilgt (disposition_reason = dup_of:#8014) — nicht vom Hash-Tor. ⇒ Die Zuordnung ist die Wurzel, nicht der Vergleich. Der einzige Hinweis auf „privat" stand im Betreff („Priv Vertrag Versicheru…"); recipient ist bei 2 von 47 Verträgen gefüllt und bei #8016nicht. ⭐ Das tragfähige Merkmal liegt verteilt über zwei Felder — #8014 „…Pers.-Abt." im Empfänger, #8016 „Priv …" im Betreff. Wer darauf baut, muss beide lesen.
⛔ Was wir NICHT messen können — die Grenze gehört in den Text: nur 298 von 712 Belegen tragen überhaupt einen Fingerabdruck: c3 156/359 · privat 57/174 · c4 44/77 · unassigned 34/36 · und zwei Gabriel-Quellen mit je 0: gabriel_cockpit_ocr 41 und gabriel_cockpit 17 (getrennt genannt, weil die Wahl der Gruppierung eine Messentscheidung ist). Über 414 Belege sagt die Hash-Frage von vornherein nichts.
⛔⛔ UND DIE GEGENRICHTUNG BAUT MAN EIN, WENN MAN NUR DIE ERSTE LÖST: schickt C4 die Rechnung per Post, kommt bei C3 der eingescannte Papierbeleg an — anderer Fingerabdruck, dasselbe Geschäft. Dann greift kein Dedup, und niemand erkennt die zwei Belege als EINEN Vorgang. Als Grössenordnung gemessen, nicht als Beweis: 13 von 34 Betrags-Gruppen (> 100 €) überspannen die Entitäts-Grenze (630,00 € über c3/c4 · 479,57 € über c3/privat). ⛔ Gleicher Betrag ist KEIN Nachweis desselben Geschäfts — die Zahl sagt nur, dass der Fall nicht hypothetisch ist.
⛔⛔ JCs FORMULIERUNG ZEIGT AUF EIN FELD, DAS ES PRAKTISCH NICHT GIBT. „Eine Rechnung von C4 an C3" verlangt beide Seiten — und counterparty_entity ist bei 7 von 712 Belegen gefüllt. Das „von X an Y" existiert in den Daten nicht. ⛔ Und die Fingerabdruck-Frage hat drei verschiedene Spalten, nicht eine: content_fingerprint 420 · raw_sha256 298 · storage_fingerprint. Jede Zahl unten nennt deshalb ihre Grundgesamtheit — die Wahl der Bezugsmenge ist eine Messentscheidung. (So erklären sich zwei richtige Zahlen zu recipient: 2 von 47 bei doc_type='Vertrag', 328 von 712 über alle Belege.)
Grundgesamtheit: 38 Gruppen mit identischem content_fingerprint (105 Belege)
Gruppen
entity trennt
13
subject trennt
15
entity ODER subject trennt
21 — nicht 13+15: die Felder ergänzen sich echt, wer nur eines liest, verliert die Hälfte
⛔ NICHT trennbar
17In den Daten steht NICHTS, was eine berechtigte Zweitausfertigung von einer echten Dublette unterscheidet. Kein Bau kann das entscheiden. Das ist keine Lücke im Konzept, sondern seine GRENZE.
Beide von JC beschriebenen Muster sind da und zählbar — von den 13 Gruppen mit abweichender Entität: 5 enthalten c3-convis-gmbh UND c4-convis-sarl (JCs „C4 an C3"), 9 enthalten privat zusammen mit einer Firma (die Kopie für die eigenen Unterlagen). ⛔ Eigene Messkorrektur, benannt: mein erster Versuch zählte die nicht trennbaren Gruppen mit count(DISTINCT subject) = 1 und kam auf 7 statt 17 — 12 Gruppen haben gar kein subject, und count(DISTINCT …) zählt NULL nicht mit. Ein Filter auf „genau einer" verliert die, die keinen haben. Die 17 stammen von PROJEKT#07 und sind nachgerechnet.
⛔⛔ DIE GEFÄHRLICHSTE ANNAHME, die dieses Konzept NICHT treffen darf: „andere Entität bei gleichem Inhalt" ist ZWEIDEUTIG. Es kann eine berechtigte Zweitausfertigung sein (JCs Fall) — oder eine Mandanten-Fehlzuordnung. Beides sieht in den Daten identisch aus. ⇒ Ein Konzept, das „andere Entität ⇒ Zweitausfertigung" setzt, würde jede Fehlzuordnung als berechtigt durchwinken, und zwar leise. ⭐ Damit ist die Regel für den Bau: „andere Entität" darf ein Beleg für VORLEGEN sein, nie für DURCHWINKEN. Genau das tut duplicate_entity_split → needs_review bereits — es entscheidet nichts, es fragt.
⭐⭐ DIE POSITIVKONTROLLE ZUR NULL — und sie beendet die Deutungsfrage: neben „0 Gruppen mit gleichem Hash bei verschiedener Entität" stehen im Ereignis-Bestand 3× skill.tgv-skill-detect-duplicate.duplicate_entity_split gegen 15× high_confidence_duplicate. Der Fall ist also dreimal aufgetreten und wurde dreimal richtig entschieden. ⇒ Die 0 war nie „kommt nicht vor" — sie war die Antwort einer Abfrage auf eine Tabelle, in der der UNIQUE die zweite Zeile verhindert. Ohne diese Gegenzahl wäre die 0 unwiderlegbar und damit wertlos gewesen.
⛔⛔ DIE INFORMATION IST ZUR LAUFZEIT DA — DREI ZEILEN ENTFERNT.intake() nimmt entity_hint als Parameter entgegen (Signatur Z. 189) und ruft _doc_lookup_by_sha in seinem eigenen Rumpf (Z. 255 und 281). Die Funktion WEISS, aus welcher Entität der Neuankömmling kommt — sie gibt es beim Sha-Lookup nur nicht weiter. ⭐ Das ist kein fehlendes Datum, sondern ein nicht gestellter Vergleich, und hier besonders scharf: nicht nur die Fähigkeit war da, auch das Argument. ⛔ Dazu limit=1ohne order — eine zweite, eigene Wunde: lägen zwei berechtigte Ausfertigungen nebeneinander, sähe das Tor eine, und welche, ist undefiniert. Eine Abfrage, die aus mehreren Kandidaten willkürlich einen zieht, ist auch dann falsch, wenn sie zufällig den richtigen zieht. Der Zielzustand heisst nicht „nimm die erste", sondern „nimm die aus DIESEM Mandanten — oder keine".
⛔ UND EINE LÜCKE IN DER GEBAUTEN UNTERSCHEIDUNG SELBST— die folgende Fassung war falsch und ist unten korrigiert (Nachtrag 27.08.):entity_split verlangt, dass beide Seiten eine entity tragen (target.get("entity") and cand.get("entity") and …). 36 Belege stehen auf unassigned. Ist eine Seite leer, ist entity_split = False ⇒ high_confidence_duplicate ⇒ der Beleg stirbt. ⭐ Ein leeres Feld beweist nur das Feld — hier wird es als „gleiche Entität" gelesen, und das ist die löschende Lesart.
⛔⛔ NACHTRAG 27.08. — DIE ZEILE DARÜBER WAR FALSCH, UND SIE STAND SCHON IM KOMPENDIUM. Behauptet war: „36 Belege stehen auf unassigned; ist eine Seite leer, stirbt der Beleg." ⛔ 'unassigned' ist ein nicht-leerer String und damit wahr — bool('unassigned' and 'c3-convis-gmbh' and 'unassigned' != 'c3-convis-gmbh') ergibt True. Die 36 lösen entity_split korrekt aus und gehen nach needs_review. ⭐ Die echte Lücke sind die entity IS NULL — gemessen 6 Belege. Dort ist der Wert falsy, entity_split = False, und der Beleg stirbt. Die Lücke existiert — sie ist sechsmal kleiner und sitzt woanders. (Fehler von TOWER#48 übergeben, von mir übernommen, von TOWER#48 selbst gefunden. Hier korrigiert statt still ersetzt, damit wer §8 gelesen hat die Änderung sieht.)
⭐⭐ UND DIE KLASSE DAHINTER IST MEHR WERT ALS DIE KORREKTUR: ein ETIKETT FÜR ABWESENHEIT IST KEIN LEERES FELD.'unassigned'sieht aus wie „keine Entität" und verhält sich wie ein Wert. ⇒ Wir führen zwei Schreibweisen für dieselbe Aussage — 'unassigned' (36) und NULL (6) — und jeder Code, der „hat keine Entität" prüft, muss beide kennen, sonst misst er die falsche Menge. ⛔ Und es hat eine zweite Schneide: zwei unassigned-Belege verglichen ergibt False (gleicher Wert) ⇒ sie gelten als dieselbe Entität, obwohl von beiden nur bekannt ist, dass man es nicht weiss. ⭐ Zwei Unbekannte sind nicht gleich. Damit sind die 36 doch betroffen — nur aus der anderen Richtung, und wieder in der löschenden.
⛔⛔ UND DIESE REGEL HAT DREI TAGE SPÄTER IHREN EIGENEN AUTOR ERWISCHT — als Beispiel hierher, nicht als Fussnote. Am 30.08. mass SICHERHEIT#04 die Belege ohne Zugehörigkeit und meldete 46; EINGABE#01 mass 59. Keine der beiden Zahlen war falsch:entity = 'unassigned' ergibt 46, entity IS NULL ergibt 13, zusammen 59. Es war exakt der Fall, den der Absatz darüber beschreibt — gemessen wurde nur eine der beiden Schreibweisen. ⭐⭐ Eine Regel im Kompendium schützt nicht vor ihrem eigenen Fall, wenn man sie beim Messen nicht mitdenkt.Wer sie aufschreibt, ist nicht dagegen geimpft — er ist nur der, der es hinterher erkennt. ⛔ Entscheid TOWER#50 (30.08.), damit es nicht ein drittes Mal passiert: die kanonische Menge ist entity IS NULL OR entity = 'unassigned', und jede Zahl über diese Menge schreibt ihre Bedingung aus — samt Zeitachse (date_scanned für die Klärung, invoice_date für das Buchhalter-Paket). Zwei Fragen, zwei Zeitachsen; wer das nicht hinschreibt, legt die Zahlen nebeneinander und hält eine für falsch.
⭐⭐ UND DIE ZWEITE FOLGE WIEGT SCHWERER ALS DIE ZAHL: auf denselben 20 Belegen ist content_fingerprint0 von 20, während der Volltext 20 von 20 vorliegt (gemessen 30.08.). Der Text ist da, der Fingerabdruck nicht — der Dubletten-Prüfer hat diese Belege nie gesehen. ⛔ Damit ist auch erklärt, warum in der August-Messung Adobe und Telekom je zweimal auftauchten (einmal bei C3, einmal ohne Firma): das war kein Urteilsfehler der Dubletten-Erkennung, sondern ein Beleg, der ihr nie vorgelegt wurde. ⭐ Dieselbe Bauform wie beim Nutzlast-Mapping (#917): die Fähigkeit läuft — an dieser Menge vorbei.
⭐⭐ UND DER STÄRKSTE BELEG FÜR DIESEN ABSCHNITT WAR BIS HIER EINE SCHLUSSFOLGERUNG — jetzt ist er gezählt: von 64 getilgten Belegen liefen 64 über DRIVE#01 content_fingerprint und 0 über den Skill. Und 20 davon hätten entity_split ausgelöst (beide Seiten tragen eine Entität, und sie ist verschieden) — sie wären also nach needs_review gegangen statt in den Papierkorb. ⭐ Die gebaute Unterscheidung hat in keinem einzigen der 64 Fälle zu Wort kommen können. Konkret: #7418 (privat, Rechnung) wurde als Dublette von #7965 (kivisai, Sonstiges) getilgt — andere Entität UND andere Belegart, genau der Fall, den der Skill vorgelegt hätte.
Und die Nebenfrage ist beantwortet, nicht offen gelassen:kivisai ist ein vorgesehener Mandant, keine Streuzeile — entities führt sechs Slugs, alle mit Mandant: anna-leonenko · c3-convis-gmbh · c4-convis-sarl · gabriel · kivisai („KIVISAI (Marke)") · privat. Die Wand-Frage stellte sich also nicht — aber sie war richtig gestellt.
⛔ EIN DRITTER FALL, nach dem niemand gefragt hat — und er ist weder Dublette noch Zweitausfertigung:11 der 38 Gruppen tragen bei identischem Inhalt eine andere BELEGART; bei 5 davon weicht zusätzlich die Entität ab. Derselbe Inhalt kann nicht zwei Belegarten haben — eine der beiden ist falsch eingeordnet.— dieser Satz ist falsch; siehe den Nachtrag direkt darunter. ⭐ Schärfster Einzelfall: #7460 = Zahlungsbeleg gegen #7478 = Steuer. Und #7754 ist darunter — der Beleg über 55.475 €, dessen Etikett schon einmal Gegenstand war (§ SICHERHEIT#04/#771). ⛔ Das gehört als EIGENER, benannter Posten hierher, nicht als Unterfall der Zweitausfertigung — sonst verschwindet es in einer Kategorie, in die es nicht gehört.
⛔⛔ NACHTRAG 27.08. — JC WIDERLEGT DEN SATZ DARÜBER AN SEINEM EIGENEN BELEG, UND ES GIBT EINEN DRITTEN ZUSTAND, DEN WEDER TOWER NOCH ICH AUF DEM ZETTEL HATTEN. JC wörtlich zu #7759: „ist in einer Datei sowohl eine Versicherungspolice mit Deckungsrahmen als auch eine Rechnung mit einem Betrag. Also ja Beleg und ja Vertrag." ⭐⭐ Ein Dokument kann mehrere Belegarten LEGITIM tragen — eine Police mit Deckungsrahmen und eine Rechnung mit Betrag in derselben Datei sind kein Widerspruch, sondern zwei Aspekte eines Schreibens. Die bisherige Zeile kannte nur zwei Zustände (richtig / falsch eingeordnet) und hat den dritten als Fehler gebucht.
⛔⛔ UND DAMIT IST DIE ZAHL 11 KEIN FEHLERBEFUND, SONDERN EINE GEMISCHTE MENGE — nachgemessen, nicht geschätzt. Von den 11 Gruppen mit abweichender Belegart stellt sich 7× eine echte Belegart gegen Sonstiges (5× Rechnung|Sonstiges, je 1× Sonstiges|Vertrag und Sonstiges|Zahlungsbeleg). ⭐ Sonstiges ist keine Belegart, sondern das Fehlen einer — das Fach für alles Übrige. Eine Aussage gegen eine Nicht-Aussage ist kein Widerspruch. Die übrigen 4 stellen zwei echte Belegarten gegeneinander: Quittung|Rechnung · Quittung|Vertrag · Rechnung|Vertrag · Steuer|Zahlungsbeleg — und das sind genau die vier Paare, bei denen eine Doppelrolle fachlich naheliegt (Quittung auf der Rechnung; Police plus Rechnung; Bescheid plus Zahlung). ⇒ 0 der 11 sind bewiesene Fehleinordnungen.
⭐⭐ UND DIE FORM IST IN BEIDEN VON JCS FÄLLEN DIESELBE — das ist stärker als die Plausibilität, weil es gemessen ist: in beiden Gruppen trägt die Vertrag-Zeile keinen Betrag und die andere Zeile den Betrag. #7493 Vertrag ohne Betrag ↔ #7759 Sonstiges mit 1.497,51; #7974/#7489/#7737 Vertrag ohne Betrag ↔ #7754 Quittung mit 55.475,11. ⭐ Die zwei Rollen sind auf die zwei Zeilen verteilt, statt dass eine der beiden falsch wäre — genau JCs „ja Beleg und ja Vertrag", nur in Spalten.
⭐⭐ FÜR DIE ENTSCHEIDUNG IST DAS EINE VERSTÄRKUNG, KEINE SCHWÄCHUNG. Wenn ein Dokument mehrere Rollen tragen kann, ist „gleicher Inhalt ⇒ Dublette" noch weniger haltbar als vorher: der Löscher vergleicht Bytes, während der Unterschied in der ROLLE sitzt — und die steht nicht in den Bytes. ⛔ Und ein Fehler von mir fällt damit mit: ich hatte #7754 in dieser Zeile als „Etikett fraglich" geführt. Es war nie fraglich; es war die zweite Rolle.
⛔ Was hier BEHAUPTET und was BELEGT ist — die Grenze stammt von EINGABE#01 und wird übernommen: „Quelltext und Datenbank gemessen, keinen Lauf gefahren und #8016nicht aufgesucht — dass es genau dieser Weg war, ist die plausibelste Erklärung, nicht ein Beleg." ⭐ Wer den Weg behaupten will, muss den Fall nachstellen; das ist ein eigener, benannter Posten. Belegt sind: die zwei Stellen und ihre Reihenfolge, der fehlende entity-Select am Tor, der mandanten-blinde UNIQUE, NACHTRAG_FELDER ohne entity, und die Zahlen oben.
Was NICHT entschieden ist (JCs Hand): ⓐ urteilt das Tor künftig nur noch findend und überlässt dem Skill die Entscheidung (Reihenfolge), oder bekommt es einen Scope? · ⓑ wird der UNIQUE um die Mandanten-Achse erweitert (Migration)? · ⓒ kippt die Voreinstellung von „ist Dublette" auf „ist Kandidat, frag nach"? — heute entscheidet der Empfänger 1 von 38 Gruppen, bei 37 schweigt er, und Schweigen wird als „ist eine Dublette" gelesen; bei einer Buchung ist die teure Richtung die andere. · ⓓ woran werden zwei Belege als EIN Vorgang erkannt, wenn die Fingerabdrücke sich unterscheiden? ⛔ Nichts davon ist gebaut, und #8016 bleibt unangetastet — Entkennzeichnen macht ihn nicht sichtbar (er liegt nur bei Google), und der Zielzustand wäre geraten.
SSOT & Schnitt-Regel:00_LEITDOKUMENT/KONZEPT_MANDANT_FIRST_CLASS.md (Datenmodell · M1–M6 · Welt A/B) · AUTH_USER_MANAGEMENT_KONZEPT.md (Login/Rollen) · Mandant-Registry/Topologie (Memory) · KONZEPT_MANDANT verwaltet/souverän. Schutz-Themen (Rollen × Stufen × Matrix · Schutz-Mechanik · Haftung/Betriebsmodi · Lücken-Landkarte) leben in 🔒 Sicherheit & Datenschutz First — hier wird verlinkt, nie doppelt gepflegt (JE-136). Farb-/Marken-Detail: 🎨 Marken & Mandanten-Farben (Kapitel dieses Konzepts).