TGV · Master-Konzept · Betrieb · Stand 2026-07-13

Datenfluss — Touch Once

Wie Daten durch TGV fließen: vom Eingang über Verstehen und Klären bis zur Auswertung — und als Wissen zurück in den Kreislauf. Das Zuhause von Bibel-Prinzip 2.

„TOUCH ONCE: 1× Input → alle Ziele bedienbar, ohne zweites Anfassen."— TGV-Bibel, Prinzip 2. Ein Beleg kommt EINMAL an — danach ist er eingeordnet, auffindbar, abgeglichen, auswertbar und Teil des KI-Wissens. Der Mensch fasst ihn nur an, wenn es eine ECHTE Ausnahme ist (Klärung).

§1Der Dokument-Fluss — vom Eingang ins Fach

1 · AnkommenSammeltore

Mail (EINE email-IMAP, Mail-Kanon) · Upload/Drag&Drop · Foto unterwegs · Konnektoren. Roh-Layer ZUERST: save_raw vor jeder Verarbeitung + Parking für Unklares (PIPELINE#01).

2 · VerstehenCascade

OCR/Extraktion → Typ, Entität (aus dem RECHNUNGSINHALT, nie der Mail-Adresse!), Betrag, Datum — mit Konfidenz je Zuordnung. Router: ORCH#01 (LLM-first, Shadow).

2b · VertiefenFach-Agent je Dokumentart

⛔ Die Triage sagt nur, WORUM es geht — nicht, was es bedeutet. Danach übernimmt der Fach-Agent seiner Dokumentart: Pflichtfelder füllen, Vertraulichkeit fachlich setzen, Fristen rechnen, Verknüpfungen knüpfen (Rechnung ↔ Vertrag ↔ Projekt). Er darf ein stärkeres Modell nutzen als die Triage (R-178). Zuständigkeit aus der Registry, nie hartcodiert — KI-First §4b. Kanon 04.08. (E-50/#303), Bau offen.

3 · KlärenKlärung & Freigaben

NUR echte Ausnahmen erreichen dich (Unknowns · Konflikt · niedrige Konfidenz · sensibel · ALLE Finanz-Schreibaktionen) — im Fall-Chat, nie im Formular.

4 · AblegenFach + Wissen

Dokument ins richtige Fach (Entität/Ordner, Stufe vererbt) + RAG-Ingest (Chunks, durchsuchbar) + Verknüpfung zur Buchung.

5 · WirkenAuswertung → Output-as-Input

Berichte/Exporte — und jede Ausgabe trägt die out-bar: ⚡ als Chat-Input · 📚 zurück ins Wissen. Der Kreislauf schließt sich.

Ehrlich: Diese Pipeline „lief historisch nur ~halb" — PIPELINE#01 baut den Intake e2e neu (Roh-Layer zuerst, JC-gated). Die Alt-vs-Neu-Analyse (v11) benennt 5 verlorene Ops-Features, die zurückkommen.

§1cDie Eingangskette, Station für Station — die lebende Fassung

§1 zeigt den Fluss in fünf Bildern. Die Stationen — welcher Punkt heute trägt, welcher bricht und welcher ungemessen ist — stehen seit dem 05.10.2026 auf einer eigenen, erzeugten Seite: Die Eingangskette. Dort sind es 8 Stationen und 34 Prüfpunkte, jeder mit Fundstelle, Messuhr und Messgrenze (FOLLOW#01, Register #3710; Heimat und Erzeuger: #3750).

⛔ Warum das eine eigene, erzeugte Seite ist und nicht ein Kapitel hier. Am 05.10.2026 war die Eingangskette mindestens siebenmal dargestellt — zwei Artefakte, drei Handblätter, zwei Konzepte — und keine Darstellung war die geltende. Zwei davon waren an diesem Tag nachweislich überholt. ⭐ Eine handgeschriebene Kette altert in Tagen. Deshalb wird der IST-Teil erzeugt (08_OPS/gen_eingangskette.py, gerufen von register-nachzug.yml bei jedem Master-Merge an einer der beiden Quellen) — und dieser Absatz hier bleibt der Zeiger, nie eine Parallel-Liste.
Zwei Schichten, streng getrennt — die Hausnarbe #3741 sagt, warum: eine Ablösung, die nur den rechenbaren Teil übernimmt, ist dort ein Verlust, wo das Urteil stand.
SOLL (welcher Schritt fehlt und warum) = Urteil, bleibt von Hand in 01_ITERATIONS/CONTROL_TOWER_HANDOFF/EINGANGSKETTE_SOLL.md, trägt Datum und Urheber, und wird wörtlich auf die Seite getragen.  ·  IST (welcher Prüfpunkt grün, rot oder ungemessen ist) = Messung, erzeugt aus 08_OPS/eingangskette.json. ⛔ Ungemessen wird nie zu rot gerundet — rot ist eine Aussage, ungemessen ist das Fehlen einer Aussage.

⭐ Dieselben Knoten und Kanten speisen den Core-Graphen: chat_engine/graph_adapter.eingangskette_kanten() liefert die Kette als Aussageart prozess — genau die Schicht, die graph_quellen.architektur() selbst als „in diesen drei Regalen nicht erklärt" meldet. Kein zweites Bild, eine Quelle, zwei Leser. ⛔ Der Endpunkt /api/skill/system-graph ruft heute nur die Architektur-Schicht; die Verdrahtung der Prozess-Schicht ist ein eigener, dienstwirksamer Vorgang.

§1bService-Mail-Intake 2026-09 — das Sammeltor „Mail“ ausgeschrieben

§1 nennt das erste Sammeltor in fünf Wörtern: „Mail (EINE email-IMAP, Mail-Kanon)“. Dahinter liegen seit August/September 2026 drei Routen im Dienst — und ihr Vertrag stand bis hierher nur im Quelltext. Das ist kein Schönheitsfehler: ohne geschriebenen Vertrag kann der Eingang doppelt laufen, gar nicht laufen, oder Mails zu früh als erledigt markieren. Dieses Kapitel schreibt ihn auf — gemessen am Code, nicht erinnert (MAIL#01, Register #1627).

„Der Stempel \Seen ist der LETZTE Schritt — und das ist die ganze Sicherung.“— Bricht der Lauf davor ab, bleibt die Mail ungelesen und wird wiederholt; der Dublettenschutz macht daraus eine Dublette statt eines Doppelbelegs. Wäre der Stempel der erste Schritt, wäre jeder Abbruch ein Verlust.

Die drei Routen — wer darf, was schaltet, wer stempelt

RouteWofürWer darfSchalterSetzt \Seen?
POST /api/skills/mail-intake/pollDer Normalbetrieb: holt UNSEEN aus dem Postfach (BODY.PEEK[] — der Server setzt selbst kein Flag). Auch von Hand als Gegenprobe mit apply=false.Global-Admin — Postfach ist Infrastruktur, nicht „jeder eingeloggte Nutzer“system_config.use_local_mail_intake muss exakt true sein, sonst 409 (fail-closed). Takt: Env TGV_MAIL_INTAKE_POLL_SEC, 0/leer = kein Hintergrund-Thread, Mindestabstand 60 s.JA — als einziger Weg
POST /api/skills/mail-intake/backfillDer Nachhol-Lauf über das ganze Postfach (ALL oder SINCE) — für Mails, die der Poll nie sehen kann, weil sie schon gelesen sind.Global-Adminapply hat hier die umgekehrte Vorgabe: FALSE. Ein Nachhol-Lauf ist per Vorgabe eine Messung, kein Eingriff. apply=true verlangt zusätzlich denselben Schalter wie der Poll.NIE — die deps werden ohne mark_seen_fn gebaut
POST /api/skills/mail-dropEine gezogene .eml geht denselben Weg wie eine geholte. Anlass (JC, 26.08.): der Weiterleitungs-Weg ist tot — Microsoft blockt externe Weiterleitung mit 550 5.7.520. Der Drop ersetzt den kaputten Weg, er ergänzt ihn nicht.Schreibrolle, nicht Global-Admin — es ist ein Nutzer-Eingang, kein Postfach-EingriffKeiner. Der Schalter regelt, wer das Postfach leert; eine gezogene Datei ist kein Poll-Lauf. Ihn daran zu hängen wären zwei Fragen an einem Schalter.NIE — mark_seen=False nimmt die Naht per Bauart weg
POST /api/skills/imap-mark-seenDer Stempel als eigene Route (n8n hat keinen selektiven IMAP-STORE). Nimmt uids und/oder message_ids.Global-AdminKeiner — aber ohne IMAP-Zugangsdaten 503 statt stillem ÜberspringenJA — das ist ihr Zweck
⭐ Die Kern-Invariante von imap-mark-seen: markiert NUR explizit übergebene UIDs (bzw. per Message-ID aufgelöste) — NIEMALS ein blankes STORE 1:*. Ein Teil-Fehlschlag darf nie die ganze Mailbox als gelesen markieren. Eine Message-ID, die nichts trifft, ist unaufgelöst und wird als Fehlschlag gemeldet, nie still verworfen; jede UID scheitert für sich, eine schlechte reißt die anderen nicht mit.

Der Weg einer Mail — die Reihenfolge IST der Vertrag

1 · HolenUNSEEN, mit Peek

SEARCH (UNSEEN) → FETCH (BODY.PEEK[]). ⛔ Eine nicht-OK-Antwort auf SEARCH wirft (seit 01.09.) — vorher wurde daraus lautlos eine leere Liste und der Lauf meldete „0 Mails · 0 Fehler“, nicht zu unterscheiden von einem leeren Postfach.

2 · EML sichernBytes vor Zeile

EINMAL je Mail wird die rohe RFC822-Datei als kind='eml' gesichert — vor allem anderen. ⛔ Scheitert sie, bricht die ganze Mail ab: nichts wird angelegt, die Mail bleibt UNSEEN, ein Ereignis sagt warum.

3 · Je AnhangRegeln → Triage → Zeile

Anhänge splitten (inkl. Mail-in-Mail) → Routing-Regeln → Triage → Entitäts-Absicherung gegen das Vokabular → documents-Zeile. Derselbe Baustein wie der Upload-Eingang (vorbereitung.vorbereite()) — nicht zwei Fassungen derselben Entscheidung.

4 · VerteilenDispatch, flag-gebunden

Nur wenn der Dispatch-Schalter steht. ⛔ Ein Fehler hier wird gefangen und in ein eigenes Feld geschrieben — siehe die Warnung unten.

5 · Stempeln\Seen — zuletzt

Nur wenn alle vier Bedingungen gelten. Scheitert der Stempel selbst, bleibt die Mail ungelesen und kommt beim nächsten Lauf wieder — der Dublettenschutz fängt sie.

⛔ \Seen — wann gestempelt werden darf, wann nicht

BedingungWarum sie da istWer sie NICHT erfüllt
① apply ist trueEin Trockenlauf schreibt nichts — auch kein Flag. Sonst wäre die Gegenprobe ein Eingriff.jeder Lauf mit apply=false
② kein Anhang-Fehler„Erledigt“ heißt: ALLE Anhänge sind gesichert und eingetragen. Ein Teil-Fehlschlag darf die Mail nicht aus dem Zulauf reißen.jede Mail mit mindestens einem status='error' — dazu feuert der Anhang-Wächter
③ eine uid liegt vorOhne UID gibt es keine Mail im Postfach, die man stempeln könnte.mail-drop (gezogene Datei)
④ mark_seen_fn ist injiziert⭐ Eine Naht, die nicht existiert, kann niemand versehentlich wieder verdrahten. Drop und Nachhol-Lauf bekommen die Funktion gar nicht erst — statt sich darauf zu verlassen, dass ③ schon greifen wird.mail-drop und backfill
⛔⛔ ZWEI STELLEN, AN DENEN TROTZDEM GESTEMPELT WIRD — und wer sie nicht kennt, liest den Zustand falsch:
① Eine per Regel GEBLOCKTE Mail wird gelesen. Ein Regel-Block ist kein Fehler, sondern eine Entscheidung — die Mail gilt als bearbeitet und bekommt ihren Stempel. Das ist richtig so, aber nur, wenn man es weiß: im Postfach sieht sie danach aus wie eine erfolgreich verarbeitete.
② Ein fehlgeschlagener Dispatch verhindert den Stempel NICHT. Der Verteiler-Aufruf ist in eine eigene Klammer gefasst und landet als Fehlertext am Ergebnis, ohne die Fehlerliste zu füllen. ⇒ Die Mail gilt als erledigt, obwohl die Kaskade nicht lief. Der Beleg existiert, die Folgearbeit nicht — und das Postfach gibt den Fall nicht mehr her.
Was bei einem Abbruch DAZWISCHEN passiert (der Fall, für den die Reihenfolge gebaut ist): Stirbt der Lauf zwischen Schritt 2 und 5 — Absturz, Netz weg, Zeitüberschreitung am Proxy —, bleibt die Mail UNSEEN und wird beim nächsten Lauf erneut geholt. Sie wird dann nicht doppelt angelegt: die Vorprüfung über die Message-ID kennt die schon eingetragenen Anhang-Indizes, und darunter liegen die Datenbank-Riegel (idx_documents_dedup, uq_documents_raw_sha256) samt 409-Klammer. ⭐ Der Preis ist eine Dublette in der Meldung, nicht ein Doppelbeleg im Bestand — und genau deshalb steht der Stempel zuletzt. Auch der umgekehrte Fall ist abgedeckt: scheitert der Stempel selbst, wird das Ereignis geschrieben, die Mail gilt als ungelesen und kommt wieder. Es gibt keinen Zustand „gestempelt, aber nicht verarbeitet“.

Der Nachhol-Lauf — und warum er absichtlich unbequem ist

EigenschaftRegelDer gemessene Grund
Obergrenze200, und kein stilles Kappen: darüber kommt 400 mit genanntem Ausweg (über seit teilen, nicht über ein größeres Limit)617 lief zweimal 20 Minuten und endete am nginx-Proxy als 504 — der Dienst lebte weiter, die Antwort war weg. 200 ging nachweislich durch. 300 liegt dazwischen und ist NICHT gemessen; wer sie anhebt, belegt es an einem Lauf.
SpeicherStrom statt Liste: eine Mail zur Zeit, egal wie groß das PostfachDie alte Fassung hielt alle geholten Mails gleichzeitig — bei Anhängen über 12 MB ein Absturz auf dem Dienst, auf dem auch KAI läuft. ⭐ Die Hälfte der alten Begründung für die 200 ist damit weggefallen; sie bleibt allein wegen der Zeit.
Erster Aufruf⭐ Immer mit Limit 1 anfangenDie Gesamtzahl des Postfachs kommt aus SEARCH, also vor dem ersten FETCH — sie ist gratis. Ein Aufruf holt eine Mail und meldet trotzdem, wie viele insgesamt liegen und wie viele abgeschnitten wurden.
Ab 10 MailsDer Lauf geht in den Hintergrund (sofern der Aufruf eine Warteschlange hat — sonst bleibt er synchron, statt eine Tür ohne Raum zu versprechen)Vier echte Läufe über je 60 Mails: schlechtester Fall 11,2 s je Mail. Gegengröße ist proxy_read_timeout 180s — 10 × 11,2 s sind bereits 62 % des Budgets.

⛔ Grenzen — was dieser Weg NICHT kann

⛔⛔ DIE MESSGRÖSSE IST UNSEEN — und genau das lässt ein volles Postfach wie ein leeres aussehen. Markiert irgendetwas anderes eine Mail als gelesen — ein Mailprogramm, ein zweiter Abholer, eine Server-Regel —, dann sieht der Poller sie nie, und alles bleibt grün. Es gibt keinen Fehler, keine Ausnahme und keine auffällige Zahl; es gibt nur eine Mail, die nie ankam. Kein Code an dieser Stelle kann die Frage beantworten — wer sie beantworten will, muss ins Postfach sehen oder den Nachhol-Lauf fahren. Das ist der einzige Weg zurück.
GrenzeWas heute giltStand
Mehrere WorkerEin Poll-Thread je Prozess. Läuft der Dienst mit mehreren Workern, pollt jeder. Doppel-Anlagen verhindern die Datenbank-Riegel — die Kosten verhindern sie nicht.bewusst, benannt
Antwort-FadenDer Faden kann am Beleg gar nicht ankommen. Die Normalisierung baut ihre Metadaten aus genau sechs Schlüsseln (from · subject · date · message_id · is_forwarded · entity_hint) — In-Reply-To und References sind nicht dabei. ⛔ Gleichzeitig liest die KAI-Mailauskunft metadata.in_reply_to (executors_mail.py:105) und druckt sonst „kein In-Reply-To / References“. Ein Leser ohne Schreiber — die Zeile ist unabhängig davon leer, was in der Mail stand.Lücke, jetzt benannt
Mail-in-Mail ≠ Fadendocuments.parent_message_id (Mig 052) ist eine andere Achse: die äußere Mail eines eingebetteten RFC822-Anhangs — nicht die Mail, auf die geantwortet wurde. Wer die Spalte für den Faden hält, sucht an der falschen Stelle.geklärt
Zwei Spalten fehlenFehlen documents.mail_to (Mig 273) oder documents.entity_quelle, läuft der Eingang weiter — sagt es aber einmal je Prozess als Ereignis, mit dem Handgriff darin. Ohne entity_quelle ist ein Wert aus einer strukturellen Regel von einem aus dem Text eines fremden Dokuments am Beleg nicht unterscheidbar.Migration = JCs Hand
Verworfene BilderInline-Bilder gelten als Signatur und werden verworfen. Seit 29.08. sagt es jedes verworfene Bild als eigenes Ereignis — das Verhalten ist unverändert, nur nicht mehr stumm. ⭐ Die Zahl wurde die ganze Zeit gerechnet und nirgends ausgesprochen.gemeldet, nicht geändert
Geprüft ohne NetzDie Drop-Kette ist an echten .eml-Bytes offline geprüft (Doppel statt Netz). Was erst der echte Zug beweist, ist das Zusammenspiel mit Storage und Triage-Gehirn im Betrieb.Messgrenze benannt

Zwei Zahlen aus dem Bestand — eingeordnet, nicht neu gemessen

⛔ 69 Belege tragen einen Anhang-Index, aber keinen Datei-Zeiger — 47 davon tragen den Mailtext, 22 sind leer, und 30 sind als Rechnung einsortiert, ohne Datei und ohne Markdown. Einordnung: es gibt einen Zweig, der per Bauart eine Zeile ohne Datei anlegt — der Mailkörper einer Mail ohne verwertbaren Anhang. Für ihn ist die fehlende Datei kein Verlust. ⛔ Für die 30 als Rechnung einsortierten ist sie einer: eine Rechnung ohne Datei ist keine Rechnung, und die 22 ganz leeren sind die härteste Klasse — weder Datei noch Text. ⇒ Die Zahl 69 ist nicht ein Befund, sondern drei, und nur einer davon ist harmlos.
⛔⛔ 0 Ereignisse „Mailkörper-Zeile angelegt“ — und diese 0 darf NICHT als „der Zweig lief nie“ gelesen werden. Der Vermerk, der diese Tatsache festhält, existiert erst seit dem 02.09.2026 (Commit 1c0dcaf9, auf master geprüft). Die 0 reicht also höchstens über die Tage seit dem Einbau — und auch das nur, soweit der laufende Dienst diese Fassung wirklich trägt. ⭐ Die 69 Belege sind älter als ihr eigener Vermerk; sie können in ihm gar nicht auftauchen. Eine 0 ohne ihr Anfangsdatum entlastet falsch — und falsche Entlastung sieht aus wie Ordnung.
⭐ Und die dritte übergebene Beobachtung, eingeordnet: zu fünf geprüften .eml steht fest — vollständiges RFC822 ja, der Beleg hängt als Anhang (nicht als Mailtext), und In-Reply-To/References fehlt in allen fünf. ⇒ Die EML-Sicherung bewahrt die Bytes vollständig; die Faden-Zugehörigkeit ist am Beleg trotzdem nicht rekonstruierbar — aus zwei voneinander unabhängigen Gründen, dem Fehlen in diesen fünf Rohmails und dem Leser ohne Schreiber eine Tabelle weiter oben.

§2Der Finanz-Fluss — die 5 Stufen (Kanon)

Der Finanzen-Kanon ist DIE Prozess-Achse der App — dieselben 5 Stufen strukturieren die 26 Pipeline-Karten im V3-Mapping (s0–s4) und den Hero-Stepper:

StufeWas passiertLanes/AnkerStand
1 · SammelnBelege + Bank-Transaktionen herein (Bank-CSV multi-mandant: 92 Tx importiert)FES-BANK#01 · PIPELINE#01Bank ✅ · Intake e2e gated
2 · KlärenAusnahmen entscheiden (Fall-Chat), Regeln lernenKlärung-Schnitt · Human First §2live (Schnitt)
3 · AbgleichenBeleg ↔ Buchung matchen; Matcher-Härtung (3 Guards, Transfer-Exclude ↔ neutral R-148)RECONCILE#01Guards gebaut
4 · AbschließenPeriodenabschluss-Lock + Buchhalter-Versand (Vor-Versand-Verifikation)FES-AP2DONE · Echtversand JC-gated
5 · AuswertenUStVA/EÜR/Cashflow/Kosten — mit Quelle an jeder Zahl (R-91)FES-AP3 (ustva-prep, C3-Pilot) · KOSTEN#01ustva-prep · Cockpits
Dedup — die Fluss-Krankheit: 3 bekannte Wurzeln für Doppel-Zählung; der Approve-Guard stoppt sie am Wirkort (STAGED), ein Sweep räumt Bestand. Test/Prod-Isolation (BR#04-Incident): E2E-Tests schrieben Testbelege in Prod — Fix = is_test-Flag + Konsumenten-Filter. Beides gehört ZUM Fluss-Kanon, nicht zur Fußnote.

§3Wo die Dateien liegen — Storage

SchichtKanonStand (ehrlich)
Datei-StorageMigration Google Drive → Hetzner (Nürnberg, EU) — mandanten-getrennt, provider-flexibel über Resolver-Abstraktion; Drive-Links bleiben gültig (GOLDENE REGEL #xxxx→Drive bis Umstellung)Migration läuft
Bestands-WahrheitJedes documents-Row hat seine Datei — Audit prüft es⚠ STORAGE#01-Audit ROT: 136/705 Docs ohne Datei · Papierkorb-Frist war ~13.07. — Heilung = Teil der Migration
Struktur-KanonAblage-Kanon (Ordner = Default-Tenor, Datei kann strenger — Stufen siehe Sicherheit §3)Kanon GELOCKT

§4Der Wissens-Fluss — Indexierung, die zwei Suchwege, das Balancing

JC-Frage (30.08.): „Haben wir einen doppelten Suchweg? Nach Index und ‚Wörtern' und nach LLM-Chunks? Wie ist das Balancing, wo geregelt?" — Antwort: Ja, es sind ZWEI Suchwege über DENSELBEN Chunk-Bestand, und das Balancing steht an genau EINER Stelle im Code. Hier der Kanon, IST-gemessen (Stand 30.08., KAI#02), jede Aussage mit Fundstelle. Grounding bleibt: jede Antwort mit Quelle (R-91), gegatet durch Mandant + Vertraulichkeit + Projekt-Wand (Mig 263); Rückweg 📚 Output-as-Input (R-34).

§4a · Indexierung — wie eine Datei Wissen wird (der Schreibweg)

Es gibt KEINEN eigenen „Indexierungslauf". Der Skill-Router ruft je Beleg tgv-skill-rag-ingest; der zerlegt den Text in Chunks à 3.200 Zeichen (~800 Tokens) (02_SKILLS/tgv-skill-rag-ingest/skill.py:129) und schreibt sie nach rag_chunks. Mit jeder Chunk-Zeile entstehen drei Indexe zugleich:

IndexWas er istWo er entsteht
Vektor (LLM-Chunks)mistral-embed (EU, 1024 Dim.) → Spalte embedding_mistral; Cosine-kNN über HNSW-Indexskill.py:422f (Embedding-Aufruf) · Mig 196 (HNSW)
Wort-Index deutschcontent_tsv, GENERATED-Spalte — füllt sich automatisch beim Insert, kein zweiter LaufMig 072
Wort-Index französischcontent_tsv_french, ebenso GENERATEDMig 125
Zwei Randnoten, ehrlich: ① Der Alt-Vektorraum embedding (OpenAI, 1536) wird seit Mig 221 nicht mehr befragt — er liegt nur noch da. ② Der Ingest-Skill setzt source_type/source_key nicht (Befund ③, skill.py:69ff, eingefroren). Bestands-/Batch-Ingest: 08_OPS/rag01/ingest_core_privat.py.

§4b · Die zwei Suchwege (der Leseweg)

WegMechanikKennzeichnung an der Karte
① Vektor („geprüft")RPC search_knowledge_scoped (aktuelle Fassung Mig 263): Frage wird mit mistral-embed eingebettet (executors_rag.py:305–317), Cosine-kNN auf embedding_mistral, Relevanz-Cutoff p_min_score=0.78 (INTERIM aus Mig 227 — Ablauf-Bedingung wäre ein Reranker, den es NICHT gibt)weg=vektor · ai_suggested
② Wortsuche („ungeprüft")RPC search_knowledge_lexical (Mig 232): websearch_to_tsquery deutsch+französisch, AND bewusst zu OR aufgeweicht (Mig 232:43–49 — sonst verfehlte „Hiscox Versicherung" den Chunk); ts_rank ORDNET nur, es gibt keinen Relevanzfilterweg=wortsuche · ai_unverified

Daneben, oft verwechselt, aber KEIN Volltext-Suchweg: query_documents ist die strukturierte Metadaten-Suche auf documents (Zeitraum · Sortierung · Limit · Betrag; executors_rka.py:394ff) — sie liest Felder, nie Text.

§4c · Das Balancing — wo geregelt

An genau einer Stelle: 08_OPS/chat_engine/executors_rag.py, make_knowledge_executor (:440–653). Die Regel heißt A″ (Tower#33, 08.08.):

RegelWortlaut
Beide immerBeide Wege laufen bei JEDER Frage — nicht erst, wenn der Vektor leer ist. („Nur bei leerem Ergebnis" ist an JCs Schlüssel-Frage widerlegt: der Vektor lieferte 10 falsche Treffer, die richtige Mail kam nie zum Zug — :564–581.)
Vektor führtDer geprüfte Weg behält sein volles Budget (top_k=6); die Wortsuche ERGÄNZT nur Dokumente, die der Vektor nicht schon hat (Dedup je Dokument, Vektor gewinnt — :595–606). Deckel gesamt: 10 Karten (:612).
Gepinnter BelegAusnahme: nur Vektorweg + harter Filter auf genau dieses Dokument (:552–563); die Wortsuche läuft dort bewusst nicht (:582).
KAI erfährt die MischungDie Trefferzeile (:418–437) sagt KAI wörtlich, wie viele Treffer UNGEPRÜFT sind und dass sie Ergänzung sind, keine Alternative.
Nach der Antwortagent_loop degradiert ungeprüfte Wortsuche-Karten, die KAIs finale Prosa nicht nennt, zur kompakten Quellen-Zeile — kennzeichnen statt filtern (#848 _degrade_unused_knowledge, #911 kartenartübergreifend).

§4d · Bekannte Schwächen — mit Beleg, nicht mit Gefühl

SchwächeMessung/Beleg
Scores trennen nichtts_rank-Abstand relevant↔irrelevant 4,6 % (Register #848, Session deb310aa); deshalb ordnet die Wortsuche nur und filtert nie — „es gibt keinen Score, der trägt" (gemessen 08.08., steht wörtlich in der Trefferzeile an KAI)
OR matcht jeden Ein-Term-Chunk„rentenversicherung JC" → jeder „jc"-Chunk matcht, idf fehlt (#848 Wurzel A). Gegenmittel GEMESSEN wirkungslos: AND-first fiele an allen drei Prüf-Queries auf OR zurück (0 AND-Treffer), Term-Bonus ändert keine Spitze → kein Bau (Register #849, 08_OPS/rag01/mess_849_*.py)
Chunk-Größe ist der echte HebelMail #8214 lag im Vektorraum Rang 451/895; dieselbe Datei in 700-Zeichen-Chunks: Rang 27 — Gegenprobe mit fremder Frage zeigt: es misst Passung, nicht Kürze (executors_rag.py:576–581 · 08_OPS/rag01/ERWARTUNG_chunk_groesse.md). Offen, kein Bau beauftragt.
Cutoff nur am Vektorwegp_min_score=0.78 schneidet NUR Vektor-Treffer; liefert er 0 (Cutoff greift), trägt allein die ungefilterte Wortsuche — exakt JCs Fall 29.08. (#848: Vektor 0, 8 Karten aus Wortsuche). Anzeige-seitig durch Degradierung (#848/#911) entschärft.
Toter dritter Indexdocuments.intake_text trägt seit Mig 053 einen GIN-Volltext-Index — in chat_engine und app.py befragt ihn niemand (R-193-Reichweite: diese zwei Korpora geprüft, 0 tsquery-Rufer; Skills lesen die Spalte nur als Daten). Ein Index ohne Leser ist Bestand, kein Suchweg.
Stand nachgeführt (30.08., KAI#02): Die frühere Lücke dieses Abschnitts („KAI erreicht die Wissensbasis nicht") ist GESCHLOSSEN — search_knowledge ist im CORE-Chat live verdrahtet (executors_rag.py, Scope-Wände Mandant/Entität/Projekt/Vertraulichkeit). Und das frühere Wort „Hybrid-Suche + Reranker" war eine Behauptung: einen Reranker gibt es nicht; er ist die benannte Ablauf-Bedingung des Interim-Cutoffs (Mig 263:262). Konzept sagt jetzt, was gebaut ist.

§5Fluss-Regeln (die Invarianten)

RegelBedeutungAnker
Roh zuerstsave_raw VOR jeder Verarbeitung — nichts geht verloren, alles reproduzierbar; Unklares parkt (Parking), verschwindet nie still. ⛔ Gemessen 08.08.2026: die Regel stand, der Weg fehlte — save_raw() existiert seit PIPELINE#01, wurde aber von keinem Eingangsweg gerufen (der documents-INSERT lag in n8n), und 724 von 724 documents-Zeilen tragen keinen Roh-Zeiger, obwohl Mig 228 die Spalten anbietet. Seit Etappe 1 gibt es den Weg: ein Service-Endpunkt sichert die Bytes und legt erst danach die Zeile an — scheitert die Sicherung, entsteht keine Zeile (fail-loud). Eine Invariante ohne Aufrufer ist ein Vorsatz.PIPELINE#01 · R-179
Entität aus dem InhaltBill-to/Lieferant aus dem RECHNUNGSINHALT entscheidet — nie die Mail-Adresse/der KanalRegister (feedback_entity)
Triage klassifiziert, Fach-Agent verstehtNach der Triage übernimmt ein Fach-Agent je Dokumentart und macht die Kette VOLLSTÄNDIG (Pflichtfelder, Vertraulichkeit fachlich, Fristen, Verknüpfungen); er darf ein stärkeres Modell nutzen — die Modellstärke folgt der Rolle, nicht nur dem Preis. Wer zuständig ist, steht an EINEM Ort (Registry), nie im Router-CodeR-178 · KI-First §4b
Stufe reist mitVertraulichkeit begleitet das Datum durch JEDEN Schritt (Filing → RAG → Retrieval → Export) — First-Class, kein Add-onSicherheit §3
Zwei Datums-AchsenEingangsdatum ≠ Rechnungsdatum — beide sichtbar, nie vermischtJE (Anatomie 05.07.)
Schreiben nur mit Freigabejede schreibende Fluss-Aktion (buchen, versenden, verschieben) läuft über die Freigabe-Karte + ProtokollR-94
Der Kreis schließt sichJede Korrektur des Users ist ein Lern-Ereignis, und jedes Lern-Ereignis muss den Weg zurück in Triage und Fach-Agent finden — sonst ist der Fluss eine Linie, keine Schleife. Heute: Erfassen ✅ live · Verdichten ⚠️ Promoter da, pattern_rules leer · Zurücklesen ❌ nur Schatten (readback.py „DRY, 0 Writes"). ⛔ Gelerntes trägt seine Scope-Grenze und seine Herkunft mit — ohne beides ist es fachlich falsch und nicht abschaltbarKI-First §4d · Register #316/#317
Quelle an jeder Zahlvom Fluss bis ins Dashboard: #xxxx immer klickbar (GOLDENE REGEL)R-91
Intervalle prüfen, nicht nur ZeilenEin Diff, der Zeilen-Identität prüft, aber nie Intervall-Konsistenz, lässt unvollständige Ersetzungen durch. Ein paarweiser Abgleich („ist DIESE Zeile schon da?") kann strukturell nicht sehen, dass zwei je unverdächtige Zeilen zusammen denselben Raum belegen. Gilt für alle Intervall-Daten (km-Stände, Zeiträume, Perioden, Gültigkeiten): nach dem Zeilen-Abgleich immer die Intervalle prüfen. Narbe: beim Fahrtenbuch-Gold-Import blieben Bestandszeilen stehen (galten als „identisch gematcht"), eine überspannende Perioden-Zeile kam als „neu" darüber → 302 km doppelt im Steuerjahr. Zweite Narbe am selben Ort: eine Baseline nur über das Datum ist eine Tarnkappe für genau das, was ein Import einschleppt — ein Nachtrag trägt ein altes Fahrt-Datum und rutscht darunter; die fehlende Achse ist die Herkunft (source).RKA#10 04.08. · Guard C4b tgv-skill-rka-kilometer · Register #305/#306

§6Datums-Semantik — welches Datum was bedeutet (gemessen 10.08., WELLE3#01)

„Datum" ist mehrdeutig — Eingang ≠ Rechnung ≠ Buchung ≠ Beginn ≠ Ende ≠ Frist. Die §5-Invariante „zwei Datums-Achsen" ist in Wahrheit eine ganze Landkarte. Der ORCH-Vertrag muss dem Fach-Skill sagen, WELCHES Datum er sieht — sonst rät er, und das Verwechseln IST der Fehler.— live gemessen (DATEN#01, 10.08.) gegen documents (665) + prf_contracts (277). Zahlen = Momentaufnahme (RECONCILE#01 löscht parallel Dubletten), die Aussage stabil.
Feld · AchseBedeutungWo · Füllgradgilt für
mail_date_receivedPostfach-Eingang (Touch-Once) — WANN kam der Beleg andocuments · 0 % ⛔ Dublette: date_received 86,6 %jeder Beleg
invoice_dateRechnungsdatum (auf dem Beleg gedruckt) — steuer-/fristrelevantdocuments · 62,3 % (das einzige tragende Datum)Rechnung · Gutschrift · Quittung
accounting_periodBuchungsmonat (YYYY-MM). Ableitung: accounting_period → invoice_date → date_receiveddocuments · 2,9 % (fast überall abgeleitet, nicht gesetzt)alle Buchungs-Typen
contract_start_date⭐ Vertragsbeginn — DER VERBINDER: ohne ihn kein Kündigungs-Stichtagprf_contracts · 2,5 % ⛔ der kritische LeerstandVertrag · Abo · Versicherung
notice_period_daysKündigungsfrist-DAUER (nicht _months!) — WIE LANGprf_contracts · 97,1 % ✅Vertrag · Abo · Versicherung
notice_deadlineKündigungs-STICHTAG — WANN (= Beginn + Dauer)prf_contracts · 8,3 % (rechenbar, sobald Beginn da ist)Vertrag
next_renewal_datenächste automatische Verlängerung (nicht renewal_date!)prf_contracts · 9,0 %Vertrag · Abo
contract_end_dateVertragsendeprf_contracts · 9,4 %Mietvertrag
⭐ Der Kernbefund — schärfer als „0 %": Die Frist ist da, das Datum nicht. Wir wissen bei 97 % der Verträge, WIE LANG die Kündigungsfrist ist (notice_period_days), aber fast nie, WANN sie abläuft (notice_deadline 8 %) — weil der Verbinder fehlt: contract_start_date steht bei 2,5 %. ⇒ Ein Fristen-Agent muss NICHT die Frist finden (die ist da) — er muss das START-Datum finden. Das ist der präzise Auftrag, den der ORCH-Vertrag (agent_loop.py:73) heute nicht stellt; ihn zu stellen ist der Kern des Feld-Lernens (KI-First §4d, KVP#01). Musterfall #7317 (BAIL DQS Mietvertrag, 107k Zeichen): Laufzeit + Indexierungsklausel stehen im Text — erkannt hat es niemand, ablegen könnte es niemand.
⛔ Vokabel-Dubletten (eine Sache, zwei Namen, ungleich befüllt) — Lese-Falle: auto_renewal 99,6 % vs auto_renews 4,3 % (wer das falsche liest, sieht 4 % Auto-Verlängerungen statt 100 %) · mail_date_received 0 % vs date_received 87 % · versicherung vs insurance in contract_type. ⭐ Dieselbe Klasse wie die deutsch/englisch-Vertraulichkeitswörter (K/#1715) und das Phantom .sensitivity vs confidentiality_level (SHELL#02) — Kandidat für einen Vokabel-Dubletten-Guard.
Arbeitsteilung (Tower#35, korrigiert): diese Landkarte ist das Bindeglied für zwei Bauten — die Extraktion des fehlenden Verbinders (VERTRAG#01 / contract-extract, präziser Auftrag: „NICHT die Frist finden — die ist da — sondern das START-Datum"; danach ist notice_deadline = contract_start_date + notice_period_days trivial und gehört in denselben Zug) · und der dauerhafte Loop, der den ORCH-Vertrag die Datums-Frage überhaupt stellen lässt (KI-First §4d/Dim. B, KVP#01). ⛔ KEIN Schema-Bau: die Felder EXISTIEREN alle — SCHEMA#01 ist raus; höchstens später ein Kanon-Dedup der Doppelvokabulare, aber ERST nach dem Vokabel-Dubletten-Guard (sonst räumen wir auf, was wir noch nicht vollständig kennen). ⭐ WIE extrahiert werden muss (DATEN#01-Stichprobe, 20 Verträge): der Beginn steht als juristische Prosa („mit Wirkung vom" · frz. „à compter du" · oder Verweis „§ 2 Laufzeit") — NIE als Feld-Etikett; ein Skill, der contract_start_date: erwartet, findet ihn nie (belegt: #6945 „Beginn der Versicherung 01.12.2003" im Text, Feld leer). Und der Enthaltungs-Fall (#7493 „Versicherungsbeginn: keine") muss leer BLEIBEN dürfen — unterscheidbar von „nicht geprüft". WELLE3#01 liefert die Semantik, ordnet keinen Bau an.

§7Der Kontakt-Fluss — EIN Stamm nach Standard (KONTAKTE#01, Register #2141, 19.09.)

Es gibt EIN Kontaktsystem für Lieferanten, Kunden, Projektpartner, Interessenten und Behörden. Es ist seit Mig 251 in Betrieb. Es wird nicht neu erfunden, sondern an ein bewährtes Modell angelehnt: Odoo res.partner ist fast deckungsgleich mit unserem Stamm. Nach außen spricht TGV vCard 4.0.— Bestand: 05_SCHEMA/251_kontakte02_crm_stamm.sql (APPLIED 16.08.) · Konzept + Bauplan: 01_ITERATIONS/KONTAKTE_01_CONTACTS/GATE_A_KONTAKTE.md §10.10
Modell (Quelle)Person ↔ FirmaTags / FilterEinwilligungWas TGV übernimmt
vCard 4.0 · RFC 6350 (+ jCard RFC 7095, CardDAV RFC 6352)KIND:individual/org/group · ORG · RELATED · MEMBER (nur bei Gruppen)CATEGORIES = Tags (§6.7.1)keineAustausch-Format: Export/Import und Handy-Adressbuch. Die DB-Struktur übernehmen wir nicht, denn vCard ist eine Karte, kein Datenmodell.
schema.org Person / OrganizationworksFor · memberOf · employeekeywordskeineVokabular für KAI-Intents und Formular-Markup, nicht für die DB.
⭐ Odoo res.partner (Open Source, LGPL)eine Tabelle für Person und Firma, is_company + parent_id (Ansprechpartner → Firma) · function (Position)category_id → res.partner.category (Tags, M:N, hierarchisch)getrennt: Blacklist/Opt-out je AdresseDas Modell. Es entspricht Mig 251 fast 1:1: kind = is_company, org_contact_id = parent_id. Neu kommt nur die Tag-Tabelle nach res.partner.category dazu.
HubSpot Contacts / CompaniesZuordnungen mit Beschriftung (Primary, „Billing contact“) — eine Person an mehreren FirmenListen (statisch/dynamisch) · Lifecycle-StageAbo-Typen je KommunikationszweckIdee: gespeicherte Filter = dynamische Liste, Zweck-Einwilligung. Deckt sich mit contact_segments + contact_consents.purpose.
⭐ Kernsatz: Rolle, Herkunft und Einwilligung sind keine Tags, sondern gemessene Felder. Rolle steht in contact_roles, Herkunft je Kanal in contact_channels.source_kind (hand · mail_header · signatur · contract_extract · formular · import), Einwilligung append-only in contact_consents. Der Filter zeigt sie als Tag-Pillen, schreibt sie aber nie doppelt. Frei vergebene Tags wie „Weihnachtskarte“ oder „VIP“ bekommen eine eigene kleine Tabelle nach Odoo-Muster. Ein Tag ist Ordnung, nie Einwilligung: „Newsletter“ als Tag berechtigt zu keinem Versand.
⛔ Grenzen, die für jeden Kontakt-Fluss gelten: Mandanten-Wand und Vertraulichkeits-Decke gelten auch beim Suchen, Zählen, Exportieren und in KAI-Antworten. Es gibt EINE Decke (contact-search _conf_visible), keine zweite Nachbildung. Vorschläge (status='vorgeschlagen') sind von Export und Versand ausgeschlossen. Der CRM-Agent baut die Liste, versendet wird nur über den EINEN Mail-Kontrakt (mail_versand.senden(), EU-Versender, JC-Entscheid 03.08.). Das geschieht erst nach Vorschau von Liste und Entwurf und nach dem ausdrücklichen OK des Menschen, nie automatisch. Serienversand „User → viele“ fehlt im Mail-Kontrakt noch (MAIL_VERSAND_CONCEPT.md:452).

§7a · JC-Entscheide 19.09. zur Screen-Vorschau (KONTAKTE#01, #2141 · JE-176)

PunktJC hat entschiedenUmsetzung nach Systematik (Reuse, nichts neu)
PCJa zur Liste, mit Detail-Panel rechts wie die anderen Listen-ScreensDas ist der Bestand: K-33 data-table (der Listen-Standard) plus K-27 dock-panel, das von rechts über die Liste gleitet, nie als geteilte Ansicht. Ein Zeilenklick tauscht den Inhalt. Das Muster nutzen schon Dokumente, Buchungen und Verträge (openDocPanel · openTxnPanel · openCtrPanel). Kontakte hat es mit openKontaktPanel (core_shell.html) bereits. Variante PC-B aus der Vorschau (feste geteilte Ansicht) entfällt.
KI-HinweisDas Zeichen „von KI ermittelt“ gefällt, bitte als Kanon-SymbolDas Kanon-Symbol gibt es schon: der goldene Funke ai-spark (K-57 „Konfidenz-Funke“, JE-161, Icon-Systematik). Das ✦ in der Vorschau war nur sein Text-Platzhalter, genauso wie in PIL-01 (controls_systematik.html). Bedeutung bei Kontakten: von KI ermittelt, also created_by_kind='ki' oder Status Vorschlag. Das ist dieselbe Bedeutung wie überall: KI-Herkunft. Eine Bedeutung, ein Symbol, kein zweites Zeichen. Als SVG gerendert, nie als Emoji (Drift-Wache DRIFT-WAND-EMOJI).
HandyVariante A: OKSuche zuerst, Karten, Filter als Bottom-Sheet, tel:/mailto: mit einem Tipp
Versand-VorschauVollständigkeit prüfen, keine zweite Mail-GestaltEs gibt EINEN Rahmen: mail_bauen() mit Signatur aus entity_signatures und Absender aus absender_kopf() (08_OPS/betrieb/mail_versand.py). Er liefert Absender, Logo, Firmenname, Adresse, Vertreter, Register-Nr. und IBAN. Im Rahmen fehlen heute: Anrede, Betreff-Vorlage, Abmeldelink, Sprachwahl DE/FR. Diese vier gehören in mail_bauen (MAIL#02), nicht in einen Serienmail-Sonderweg. ⛔ Die Magic-Link-Mail ist kein Vorbild, denn sie versendet Supabase Auth und nicht unsere Naht.

§7b · Wann darf TGV einem Kontakt schreiben? Rechtslage DE und FR (Recherche 19.09.2026)

Entscheidend ist nicht, wer der Empfänger ist (Projektpartner, Kunde, Signatur-Kontakt), sondern was die Mail ist. Es gibt drei Arten.— keine Rechtsberatung; Quellen am Ende dieses Abschnitts, abgerufen 19.09.2026
Art der MailBeispielDeutschlandFrankreich
① GeschäftskorrespondenzProjekt-Abstimmung, Rechnung, Info „neue Kontonummer“, Antwort auf eine Anfragefrei keine Werbung → kein § 7 UWG. Rechtsgrundlage DSGVO Art. 6 (1) b (Vertrag) oder f (berechtigtes Interesse)frei keine Prospection → kein L34-5 CPCE
② Gruß ohne AngebotWeihnachtsgruß, Neujahrskartezählt als Werbung Der Werbe-Begriff ist weit: selbst eine Zufriedenheits-Umfrage in einer Rechnungs-Mail ist Werbung (BGH VI ZR 225/17). Datenschutzaufsichten stufen Grüße als Imagewerbung ein. Also wie ③.zählt als Prospection L34-5: jede Nachricht, die direkt oder indirekt das Image des Unternehmens fördert. Also wie ③.
③ Werbung / NewsletterSeminar-Einladung, Angebot, Rundbrief§ 7 Abs. 2 Nr. 2 UWG: vorherige ausdrückliche Einwilligung, auch gegenüber Firmen (B2B). Ausnahme § 7 Abs. 3 (Bestandskunde), nur wenn alle vier gelten: Adresse beim Verkauf erhalten · eigene ähnliche Leistung · kein Widerspruch · Hinweis auf Widerspruchsrecht bei Erhebung und in jeder MailPrivatperson: Einwilligung, Ausnahme Kunde + ähnliche Leistung. Firmenkontakt (B2B): ohne Einwilligung erlaubt, wenn das Thema zum Beruf des Empfängers passt, er bei der Erhebung informiert wurde und jede Mail einen einfachen, kostenlosen Abmeldeweg und den Absender nennt (CNIL)
Was das für JCs Annahme heißt. „Projektpartner und alle, die uns geschrieben haben, sind Geschäftskontakte“ stimmt für ①: ihnen darf man ohne Einwilligung schreiben, was zum Geschäft gehört. Für ② und ③ gilt es nicht. In DE braucht auch der Weihnachtsgruß an einen Projektpartner eine Einwilligung oder den vollständigen Bestandskunden-Fall. In FR geht er an Firmenkontakte, wenn sie bei der Erhebung informiert wurden. Ein Signatur-Kontakt wurde nie informiert (er hat uns nur geschrieben), deshalb gilt für ihn in beiden Ländern: nur ①, bis er eingewilligt hat. Maßgeblich ist das Land des Empfängers. Im Zweifel gilt die strengere Regel (DE), fail-closed. Hinweis: C3 ist laut Mig 233:70 die „Convis Consult & Marketing GmbH“, C4 die SARL. Die Regel hängt am Empfänger, nicht an der Rechtsform des Absenders.
So erklärt TGV die Regel dem User (Text für Vorschau und KAI):
„Geschäftliches darfst du immer schicken: Rechnungen, Projekt-Infos, neue Kontodaten. Grüße, Einladungen und Werbung sind etwas anderes. Dafür braucht TGV eine Einwilligung des Empfängers, bei Kunden in Deutschland reicht auch ein Hinweis auf das Widerspruchsrecht. Wer keine hat, steht unten in der Liste ‚nicht dabei‘, mit Grund. Jede Werbe- und Gruß-Mail trägt automatisch einen Abmelde-Link.“
Wirkung im Bau: Die Empfängerliste (contact-search recipients) fragt künftig nach der Art der Mail. Bei ① prüft sie keine Einwilligung. Bei ②/③ gilt: Einwilligung zum Zweck, oder (DE) der Bestandskunden-Fall mit Hinweis, oder (FR-B2B) Information + Abmeldeweg. Wer fehlt, steht mit Grund in „nicht dabei“. Die Regel liegt in der Maschine, nicht im Gedächtnis des Users. Empfehlung zu K2 angepasst (siehe §7c).
Quellen (abgerufen 19.09.2026): § 7 UWG (gesetze-im-internet.de) · CNIL, Prospection commerciale par courrier électronique (Stand 10.06.2026) · Art. L34-5 CPCE (Légifrance) · BGH 10.07.2018, VI ZR 225/17 · BGH 15.12.2015, VI ZR 134/15 (Werbung in Autoreply) · Schwenke, Weihnachtsgrüße und DSGVO (01.10.2025). Ein Urteil speziell zu reinen Weihnachtsgrüßen per Mail wurde nicht gefunden (gesucht: Rechtsprechung UWG § 7 Weihnachtsmail). Die Einstufung als Werbung folgt aus dem weiten BGH-Begriff und der Aufsichtspraxis.

§7c · Die drei Fragen K1–K3, einfach erklärt — ✅ von JC entschieden 19.09. (JE-176)

FrageWorum es geht, mit BeispielEmpfehlung → JC-Entscheid
K1 · Handy-Adressbuch: vCard-Download oder CardDAV?vCard ist eine Visitenkarten-Datei (.vcf). Du tippst in TGV auf „vCard“, das Handy fragt „Kontakt hinzufügen?“, fertig. Das ist eine Kopie. Ändert sich später die Nummer der Bank, lädst du die Karte neu.
CardDAV ist ein Abo: das Handy hält eine Verbindung zu TGV und gleicht selbst ab, wie ein geteilter Kalender. Bequemer, aber TGV müsste dafür einen eigenen Adressbuch-Server betreiben. Kann das Handy auch zurückschreiben, landen ungeprüfte Änderungen am Klär-Weg vorbei im Stamm. Das ist dieselbe Dubletten-Gefahr, die schon einmal eine Reparatur-Migration gekostet hat (Mig 098).
✅ JC: OK Jetzt vCard-Download (ist gebaut und wird vollständiger: Firma, Position, Tags). CardDAV später und nur lesend, falls du den Abgleich im Alltag vermisst.
K2 · Wem darf TGV einen Gruß oder eine Einladung schicken?Siehe §7b. Beispiel: Projektpartner Herr M. (Firma in Lyon) und Frau K. (Firma in Köln) sollen einen Weihnachtsgruß bekommen. Frau K. braucht in DE eine Einwilligung oder den Bestandskunden-Fall. Herr M. darf ihn in FR als Firmenkontakt bekommen, wenn er bei der Erhebung informiert wurde. Beide bekommen einen Abmelde-Link. Die neue Kontonummer dürfen beide ohne Weiteres bekommen, denn das ist Korrespondenz. (Namen sind Platzhalter.)✅ JC: gesetzt („finde ich mega streng, aber ist gesetzt“) Angepasst: Korrespondenz immer. Gruß und Werbung nur mit Einwilligung, (DE) Bestandskunde mit Hinweis oder (FR-Firma) Information + Abmeldeweg. Signatur-Kontakte nur Korrespondenz, bis sie einwilligen. Die Liste zeigt je Empfänger den Grund.
K3 · Tags-SQL fahren?Heute kann man Kontakte nach Rolle, Herkunft und Einwilligung filtern, aber keine eigenen Etiketten wie „Weihnachtskarte“ oder „VIP“ vergeben. Das SQL legt zwei kleine Tabellen dafür an (Muster Odoo). Dazu kommen zwei Felder: die Position einer Person („Einkauf“, „Geschäftsführung“) und die Verbindung Rechnungs-Kunde → Kontakt. Die macht dein Beispiel „alle, denen C3 eine Rechnung geschickt hat“ sicher statt nur per Mail-Abgleich. Es wird nichts gelöscht oder geändert, nur ergänzt, und es gibt einen Rückweg.✅ JC: Ja, JC fährt das SQL, nach dem Merge von #2813. Datei: 05_SCHEMA/284_kontakte01_tags.sql (Mig 284, angewandt von JC am 19.09.)

§7d · Kontakte und Mail-Versand: passt das Modell zum Anbieter? (gemessen 19.09., mit MAIL#02)

Befund: Der Anbieter heute ist Resend (08_OPS/betrieb/mail_buchfuehrung.py:63). Brevo ist nicht eingebunden, es ist die Empfehlung in der Vorlage an JC (MAIL_VERSAND_CONCEPT.md §N5). Die Anbieterwahl ist JCs Entscheid. Deshalb bleibt das Kontaktmodell anbieterneutral und ist mit jedem EU-Anbieter kompatibel, weil es ihn gar nicht kennt:
Kontakt-Sync: keiner. Die Naht sendet je Empfänger (mail_versand.senden()), Listen und Segmente bleiben bei uns. So gibt es keinen zweiten Datenort für Adressen.
Serienmail: ist als Fall mailing angelegt, Klasse 4 „User → viele“, Freigabe mensch_mit_liste (mail_use_cases.json). Die Empfängerliste mit Vorschau ist genau diese Freigabe-Stufe.
Abmeldung: fehlt (kein Abmelde-Endpunkt, kein List-Unsubscribe). Ziel: der Endpunkt gehört TGV. Ein Klick schreibt eine neue contact_consents-Zeile widerrufen, evidence anbieterneutral (abmeldung:<outbound_mail_id>).
Rückläufer: gebaut mail_rueckkanal.py schreibt outbound_mails.bounced_at. fehlt dahinter: ein harter Bounce setzt den Kanal auf veraltet, eine Spam-Beschwerde zählt als Widerruf.
Eigner: Abmeldung, Anrede/Footer im Rahmen und Bounce→Kanal liegen bei MAIL#02, der Kontakt-Teil (Consent-Zeile, Kanal-Status) bei KONTAKTE. Nichts davon ist gebaut. Die drei Voraussetzungen der Serienmail (V1 Abmeldung · V2 Anrede und Footer in mail_bauen() · V3 Bounce/Beschwerde → Kontakt) stehen in MAIL_VERSAND_CONCEPT.md §J5 (MAIL#02, Register #2142).
Nachtrag EXPAT#01 (11.10., Register #4106): fünf Lehren aus dem Expats-Mail-Betrieb, gegen die Naht gemessen.
① Sperrliste „im Zweifel kein Versand“: ist die Liste nicht lesbar, bricht die Serienmail ab und sendet nicht ohne Liste. Ziel für die Prüfung in senden() nach §J5a, Eigner MAIL#02 (#2142).
② Abmeldung ist ein Prozess: „stopp“-Antworten landeten bei Expats in einem Postfach, das niemand liest. Ohne Empfang (Klasse 5, MAIL_VERSAND_CONCEPT.md §K) bleibt eine Abmeldung per Antwort ungehört.
③ Doppelversand-Sperre: ein gleicher Text an denselben Empfänger wird abgewiesen. fehlt in senden(). Die n8n-Idempotenz hat einen Fehler: engine.js:249 legt den Schlüssel ab, :300 überschreibt ihn. ⇒ #4111.
④ Robuster Versand: drei Versuche, Dateinamen nach RFC 2231, Anhänge im Versandbuch. fehlt: senden_gebucht versucht es einmal (mail_buchfuehrung.py:544). ⇒ #4109.
⑤ Info-Kopie: die Empfänger von Meldungen stehen als Konstante im Code (cron_mail_failure_notify.py:116, cron_health_check.py:73). Ziel ist eine Einstellung, abschaltbar ohne Deploy. ⇒ in #4109.
Schon da und bewährt: „Test = echte Mail“ (check_vorschau_ist_die_mail.py). Die Tabelle mit allen Fundstellen steht in KONZEPT_WEBSITE_FACTORY.md §4.2b.

§8Der Tabellen-Fluss — CSV und Excel in zwei Klassen (TABELLEN#01, Register #4053, 11.10.)

JC 11.10.: „Sollte da nicht ein Agent mit LLM in der Lage sein, neue Datenschemata zu erkennen und zu behandeln?“ — Für Bank-CSV gibt es ihn schon. tgv-skill-banking-import erkennt ein unbekanntes Bankformat über das EU-Modell und parst danach deterministisch. Für Stunden, KM, Projektdaten und jede Excel-Datei gibt es ihn nicht. Dieser Abschnitt misst den Bestand, trennt zwei Klassen von Tabellen und beschreibt EINEN Schema-Erkenner als Baustein, der die Bank-Erkennung verallgemeinert, statt eine zweite daneben zu bauen.— gemessen 11.10.2026 am Code (origin/master 3b7ebacc1) und an der Live-DB (nur SELECT); Probe nur mit Testdaten

§8a · Bestand je Tabellen-Art — gemessen, nicht erinnert

ArtEingang → WeicheErkennungZiel · Wand · DublettenWer liest esStand
Bank-CSVUpload · Mail · Drive → dispatch.py:720-735 (Bank-Weg bei target_stream, csv_type='banking', Name umsaetze / de\d{8,})① Regeln classify-csv/skill.py:207-266 (BV_DE · BNP_FR · GENERIC) ② Gehirn banking-import/skill.py:455: Format-Register, sonst EU-LLM (DETECT_PROMPT :498 · llm_detect_format :534)txn_master · Entität aus dem Konto-Stammsatz (resolve_entity :674) · UNIQUE + _upsert_txn :1060 · LLM-Zeilen immer needs_review (:947) · unbekanntes Konto ⇒ Stammsatz-Vorschlag, inaktiv (:1177)Buchungen · Konten · Auswertungen · Abgleich · Periode; reconcile-payment u. a.; KAI get_monthly_summaryträgt gelerntes Format nur im Prozess-Speicher (_LLM_FMT_CACHE :527): nach Neustart neu gefragt, nie vom Menschen bestätigt
Stunden-CSVnur Name (timetrack/timesheet/stunden) oder target_stream (dispatch.py:736-739); csv_type='timetrack' wird nicht gelesenRegeln classify-csv :241-247 (zweisprachig, 80–90 %)prf_timetrack · Spalten fest nach Position, nur ; (import-timetrack-csv/skill.py:128-160) · keine Mandanten-Spalte (live: 55 Zeilen, alle ohne Projekt; RLS nur Global-Admin) · UNIQUE (benutzername, datum, startzeit)niemand (gesucht: prf_timetrack in allen *.py/html/js/yaml/sql: nur Schreiber, Schema, Test)Probe: Toggl-Export ⇒ „timetrack 90 %“, dann 0 Zeilen übernommen; andere Spaltenreihenfolge ⇒ 0 Zeilen
KM / Fahrtenkein Datei-Eingang im Dienst; Erfassung im Chat/Formular (rka-kilometer :757 · rka-auto-modus :703)— (kein KM-Zweig in classify-csv)fahrtenbuch (Entität NOT NULL, 176_rka05_fahrtenbuch.sql:32); JCs Fahrtenbuch-Excel nur über Einmal-Skripte (08_OPS/ingest_fahrtenbuch_history.py, openpyxl, Hand-Lauf)Fahrtenbuch-Screen · commute-table (liest nur) · rka-calculate-pauschalen · KAI get_pendlerpauschaleErfassen ✅ · Datei-Import ✗
Projektdaten—Probe ⇒ unknown / 30— (prf_projects schreibt nur project-approve)—fehlt
Excel (jede Art)OFFICE_EXT (dispatch.py:458) ⇒ parse-doclingkeine: nur .xlsx wird Markdown-Text für die Suche; .xls/.ods ⇒ mime_unsupportedkein Tabellen-Import; ein Excel-Kontoauszug wird auf Sonstiges/UNBEKANNT gesetzt (dispatch.py:679-687)Suche / RAGlive: 5 .xlsx-Belege, keiner als Tabelle gelesen
Unbekannte CSVcsv_triage (dispatch.py:740-742)unknown / 30, kein _suggestion, keine agent_decisions (classify-csv :686)bleibt in der Ablageniemand liest unknown, keine Klärkartelive: 37 CSV ohne csv_type, 2 unknown
Anhang im KAI-Gesprächderselbe Intake (core_shell.html:24842-24860); der Thread hält den Beleg (app.py:4336-4357)wie obenwie obenKAI sieht nur, was die Pipeline schrieb; er öffnet die Tabelle nicht selbstkeine Analyse als Arbeitsmaterial

§8b · Zwei Klassen — die Unterscheidung, die JC verlangt

Klasse 1 · BuchhaltungsrelevantKlasse 2 · Arbeitsmaterial
BeispieleKontoauszug · Stundennachweis · Fahrtenbuch/KM · später RechnungslistenProjektdaten · Auswertungen · Listen, die KAI im Gespräch rechnen, vergleichen, zeichnen soll
Zieleine Fach-Tabelle mit festem Schema (txn_master · prf_timetrack · fahrtenbuch)keine Buchungs-Tabelle. Die Datei bleibt Beleg am Projekt/Thread; das Ergebnis ist ein Artefakt nach R-34 (⇩ PDF · ⇩ Excel · in Wissen)
PflichtenMandanten-Wand je Zeile (Entität aus Stammsatz/Mitgliedschaft, Default-Deny) · Dubletten-Schlüssel · Herkunft je Zeile (document_id, source_file) · Prüf-Status needs_review · GoBD: nach Freigabe unveränderbarBeleg-Sicht = Mandanten-Wand + Vertraulichkeitsstufe des Belegs, auch in der KAI-Antwort · schreibt nie in Klasse-1-Tabellen
Wer entscheidet die KlasseDer Erkenner schlägt vor, der Mensch bestätigt beim ersten Mal je Schema. Danach entscheidet das bestätigte Schema, ohne Frage und ohne Modell. Im Zweifel ist eine Tabelle Klasse 2: ein falscher Import erzeugt falsche Buchungen, eine falsche Analyse nur eine falsche Antwort.

§8c · Der Schema-Erkenner — EIN Baustein, verallgemeinert aus der Bank-Erkennung

Fünf Schritte, alle aus vorhandenen Teilen:
① Signatur. Kopf normalisiert (Ziffern ⇒ #) wie _fmt_cache_key (banking-import :530). Ist sie bestätigt bekannt ⇒ deterministisch parsen, kein Modell, keine Frage.
② Vorschlag. Unbekannt ⇒ EU-Modell über die Haus-Naht chat_engine/llm_client.py::MistralClient (fail-loud, nie ein US-Anbieter; Prinzip 5). Es liest Kopf und wenige Zeilen und antwortet mit JSON: Klasse, Ziel-Tabelle, Spalten-Zuordnung, Datums-/Dezimalformat, Konto-Kennung, Konfidenz. Der DETECT_PROMPT wird dafür nicht kopiert, sondern um das Ziel erweitert. Die Regeln aus classify-csv bleiben als billiger Vorfilter; über Unbekanntes entscheiden sie nicht mehr.
③ Probe-Parse. 5 Zeilen deterministisch nach dem Vorschlag (parse_rows_generic :554, verallgemeinert). Der Mensch sieht Werte, keine Spaltennummern.
④ Klärkarte. Vorschlag und Vorschau als Karte (tgv-skill-klaerung _doc_card :396 · Fälligkeit in orch_apply.py): „Das sieht aus wie ein Stundennachweis aus Toggl. Übernehmen · Spalte korrigieren · nur als Arbeitsmaterial“.
⑤ Gedächtnis. Die Bestätigung wird ein bestätigtes Schema (Muster agent_decisions … user_confirmed, Rückmeldung über _record_decision :219 in learning_events). Ab dann ist es Reuse. Das ist der Gedächtnis-Erkenner aus KI-First, angewandt auf Tabellen, kein zweiter.
Excel läuft durch denselben Erkenner: ein Blatt wird zu Zeilen, danach ist es ein CSV-Fall. Keine zweite Erkennung für Excel.
⛔ Grenzen: Das Modell schlägt eine Zuordnung vor, es schreibt nie eine Buchung. Geschrieben wird deterministisch nach einem bestätigten Schema, durch dieselben Wächter wie heute (Entität aus dem Stammsatz, Dubletten, needs_review). Ans Modell gehen nur Kopf und wenige Zeilen. Ein neues Ziel (z. B. eine Projektdaten-Tabelle) ist ein Schema-Entscheid und braucht JCs Wort; der Erkenner erfindet keine Tabellen.
Stand B1 (JC „GO B1“ 11.10., Register #4060 · Wand vorgezogen #4062): gebaut, PR offen — 08_OPS/tgv-skills-service/tabellen_schema.py (Signatur · EU-Vorschlag, geprüft statt geglaubt · Lesen nach Spalten-NAMEN · Probe · Gedächtnis in agent_decisions decision_type='import_schema') · tgv-skill-import-timetrack-csv 0.2.0 (mit Beleg immer der Erkenner; ohne Beleg unverändert) · tgv-skill-klaerung 0.10.0 (Schema-Karte · Bestätigen = Schema → Import → Beleg, scheitert der Import, ist alles zurück · Zurücknehmen trifft genau die Zeilen des Belegs) · dispatch.py 0.9.8 (Nicht-Bank-CSV bekommt den Erkenner statt liegenzubleiben; csv_type='timetrack' zählt). Absicht des Nutzers (JC: „immer unter Input-Intention des Users“) geht in den Vorschlag: Aufruf-Feld absicht · user_annotation · mail_subject · intake_text (Mailtext), gekürzt auf 600 Zeichen. Zuordnung JE ZEILE über das PROJEKT (JC: „eine Stundenzeile kann verschiedene Mandanten treffen“ · „Es kann nur durch die Identifikation der Projekte. Diese sind zu einer Entität verbunden, die Entität zu Mandant.“): eine Spalte + Tafel Wert → prf_projects.id, bestätigt auf der Karte; die Entität ist prf_projects.entity_key, der Mandant folgt aus ihr (Trigger Mig 313). Eine freie Tafel Wert → Entität gibt es nicht. Erlaubt sind nur Projekte, deren Entität der Beleg tragen darf (erlaubte_projekte_fuer_beleg; gemessen 11.10.: 8 Projekte in 4 Mandanten, eines mit ungültigem entity_key gabriel-binetti fällt heraus; von 55 Altzeilen fand KEINE ihr Projekt). Zeilen ohne Projekt bleiben ohne Organisation — nur Global-Admin. B1.1 (#4117, gemessen an JCs #8482): das Modell bekommt die Werte der GANZEN Datei je Spalte mit vorgerechneten Kandidaten (Code ohne führende Null · gemeinsames Wort) und beantwortet JEDEN Wert; ein Schlüssel ohne Vorkommen oder eine Zuordnung ohne gemeinsames Wort (gleiche Nummer, anderer Name) wird ein Befund — Zweifel geht in die Klärung, nie still in die Buchung; die Quote ist gemessen, nicht vom Modell behauptet. Die erlaubten Entitäten liefert erlaubte_entitaeten_fuer_beleg; es fragt user_mandant_roles: wer verwaltet den Mandanten des Belegs, welche Mandanten verwaltet dieselbe Person noch (wie is_mandant_admin). Ein fremder Mandant mit gleichem Export-Kopf bekommt keine einzige C3-Zeile. Lesewand je Zeile: STAGED 05_SCHEMA/313_…_STAGED.sql (Verwalter sehen die Zeilen ihrer Mandanten, Viewer und Fremde 0, ohne Mandant nur Global-Admin). B1-Grenzen: „Spalte korrigieren“ nur über Zurücknehmen + KAI (ein KAI-Werkzeug zum Umbiegen kommt mit B3) · „nur Arbeitsmaterial“ = B3 · erkennt er Bank oder Fahrten, merkt er nur den Vorschlag (B2).
Befunde neben dem Konzept (gehören ihrem Eigner): tgv-skill-auto-promote-banking-csv ist ruhend und ohne Rufer (_registry.yaml:890-895) — Austragen ändert den erzeugten _FK_LEITER in core_shell.html, deshalb eigene Zeile mit dem nächsten App-Deploy (TOWER#71) · credentials/_storage.normalize_entity_hint (:838-847) kennt nur sechs fest eingetragene Slugs — ein Beleg einer NEUEN Entität landet im unassigned-Eimer, gleich was entities.bucket sagt (Hardcode; trifft wbz-eg, sobald Dateien für sie ankommen).

§8d · Eine Entität zieht in einen eigenen Mandanten — ohne Datenbruch (gemessen 11.10. an „Wir Bauen Zukunft eG“)

Anlass: JC 11.10.: „Wir Bauen Zukunft eG als Arbeitgeber von JC (PRIV) … Entität von Mandant JC. Aber WbZ eG wird auch später Mandant selbst … richtig anlegen.“ Angelegt wird sie als wbz-eg im Mandanten jcbinetti (STAGED 05_SCHEMA/314_tabellen01_entitaet_wbz_eg_STAGED.sql, ohne erfundene Stammdaten). Vorbild für „eigener Mandant“: anna-leonenko, gabriel, kivisai — jede Entität in ihrem eigenen Mandanten.
Was den Umzug trägt (live gemessen, nur SELECT): ① Der Slug bleibt für immer. 20 Tabellen tragen entity UND mandant_id (u. a. documents, fahrtenbuch, projects, rag_chunks, agent_events, prf_contracts); der Slug ist ihr Schlüssel, die Mandanten-Nummer eine Ableitung. ② entities ist UNIQUE (mandant_id, slug); der einzige Fremdschlüssel auf sie ist persona_profiles (mandant_id, entity) ON UPDATE CASCADE — er folgt von selbst. ③ Vier Tabellen leiten mandant_id per Trigger ab (documents · contract_positions · kb_domains · prf_contracts), prf_timetrack mit Mig 313 als fünfte — sie leiten neu ab, sobald ihre Zeile berührt wird. ④ Dateien bleiben lesbar: der Zeiger (storage_bucket/raw_key) steht JE BELEG, nicht je Entität.
Der Umzugsweg (ein Blatt, Riegel + Gegenprobe, Tower fährt auf JCs Wort): ① neuer Mandant + Verwalter in user_mandant_roles · ② UPDATE entities SET mandant_id = <neu> WHERE slug = 'wbz-eg' (persona_profiles folgt) · ③ Trigger-Tabellen berühren (UPDATE … SET entity = entity WHERE entity = 'wbz-eg') · ④ übrige Tabellen mit mandant_id-Kopie ausdrücklich nachziehen, je Tabelle gezählt (vorher = nachher) · ⑤ künftige Dateien: eigener Eimer in entities.bucket — ⛔ erst wirksam, wenn der normalize_entity_hint-Befund oben behoben ist · ⑥ Gegenprobe: JC sieht weiter, wer nur in jcbinetti ist, sieht die neuen Mandanten-Zeilen nicht mehr. Weg beschrieben, nicht gebaut

§8e · Andere Formate strukturierter Daten — benannt, nicht gebaut

JC 11.10.: „Wir haben es hier für CSV/EXCEL/GOOGLE SHEET überlegt. Es könnte aber andere Formate von strukturierten Daten sein.“ Was heute ankommt (Live-Zählung documents nach Dateiendung, 11.10.): csv 66 · xlsx 5 · xml 1 · JSON, TSV, ODS, ICS, VCF, Bankformate (CAMT, MT940, OFX) 0; Google-Sheets-Links im Mailtext 0.
Der Weg für jedes weitere Format ist derselbe: ein Leser macht aus der Datei Zeilen mit Kopf (Excel-Blatt, Google-Sheet-Export, JSON-Liste von Objekten, XML-Wiederholungselement), danach ist es ein CSV-Fall für denselben Erkenner — Signatur, Vorschlag, Klärkarte, Gedächtnis. Kein zweiter Erkenner je Format. Formate mit eigenem Standard (CAMT/MT940 für Bank, iCal für Termine, vCard für Kontakte) bekommen ihren deterministischen Leser, ohne Modell — sie haben ein Schema, das man nicht erraten muss. In B1 gebaut: nur CSV. Excel = B2.
🗑 Löschen einer Person (DSGVO#01, Register #2145): Wenn ein Kontakt oder User seine Daten löschen lässt, gilt Sicherheit §7a. Jeder Mandant löscht nur seine Zeilen. Ohne Beleg-Bezug wird gelöscht; mit Beleg-Bezug wird gesperrt, bis die Frist aus aufbewahrung_kanon.yaml abgelaufen ist. Der Einwilligungs-Nachweis bleibt minimal im Löschprotokoll. Eine Sperrliste (HMAC je Mandant) verhindert, dass die Person über Signatur oder Import wiederkommt.
Brücken: Die 5 Finanz-Stufen sind der Hero-Stepper der App (Generative Seiten, V3-Mapping s0–s4) · der Fluss endet im Wissen, das Chatbot First verspricht · die Stufen-Wände kommen aus Sicherheit First · getrennt je Mandant (Multi-Mandant). SSOTs/Memory: Bibel Prinzip 2 · project_tgv_document_pipeline · project_finanzen_pipeline_kanon · project_pipeline01_intake_lane · project_storage_migration_drive_to_hetzner · project_storage01_lane (⚠ROT) · project_rag01_knowledge_retrieval · reference_mail_delivery_canon · project_dedup_mechanism_gaps · project_test_prod_isolation_gap · project_rka10_fahrtenbuch_db_lane.