TGV · Master-Konzept · Betrieb · Stand 2026-07-13
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.
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).
OCR/Extraktion → Typ, Entität (aus dem RECHNUNGSINHALT, nie der Mail-Adresse!), Betrag, Datum — mit Konfidenz je Zuordnung. Router: ORCH#01 (LLM-first, Shadow).
⛔ 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.
NUR echte Ausnahmen erreichen dich (Unknowns · Konflikt · niedrige Konfidenz · sensibel · ALLE Finanz-Schreibaktionen) — im Fall-Chat, nie im Formular.
Dokument ins richtige Fach (Entität/Ordner, Stufe vererbt) + RAG-Ingest (Chunks, durchsuchbar) + Verknüpfung zur Buchung.
Berichte/Exporte — und jede Ausgabe trägt die out-bar: ⚡ als Chat-Input · 📚 zurück ins Wissen. Der Kreislauf schließt sich.
§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).
⭐ 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.
§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).
| Route | Wofür | Wer darf | Schalter | Setzt \Seen? |
|---|---|---|---|---|
| POST /api/skills/mail-intake/poll | Der 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/backfill | Der Nachhol-Lauf über das ganze Postfach (ALL oder SINCE) — für Mails, die der Poll nie sehen kann, weil sie schon gelesen sind. | Global-Admin | apply 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-drop | Eine 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-Eingriff | Keiner. 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-seen | Der Stempel als eigene Route (n8n hat keinen selektiven IMAP-STORE). Nimmt uids und/oder message_ids. | Global-Admin | Keiner — aber ohne IMAP-Zugangsdaten 503 statt stillem Überspringen | JA — das ist ihr Zweck |
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.
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.
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.
Nur wenn der Dispatch-Schalter steht. ⛔ Ein Fehler hier wird gefangen und in ein eigenes Feld geschrieben — siehe die Warnung unten.
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.
| Bedingung | Warum sie da ist | Wer sie NICHT erfüllt |
|---|---|---|
| ① apply ist true | Ein 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 vor | Ohne 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 |
| Eigenschaft | Regel | Der gemessene Grund |
|---|---|---|
| Obergrenze | 200, 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. |
| Speicher | Strom statt Liste: eine Mail zur Zeit, egal wie groß das Postfach | Die 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 anfangen | Die 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 Mails | Der 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. |
| Grenze | Was heute gilt | Stand |
|---|---|---|
| Mehrere Worker | Ein 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-Faden | Der 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 ≠ Faden | documents.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 fehlen | Fehlen 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 Bilder | Inline-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 Netz | Die 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 |
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:
| Stufe | Was passiert | Lanes/Anker | Stand |
|---|---|---|---|
| 1 · Sammeln | Belege + Bank-Transaktionen herein (Bank-CSV multi-mandant: 92 Tx importiert) | FES-BANK#01 · PIPELINE#01 | Bank ✅ · Intake e2e gated |
| 2 · Klären | Ausnahmen entscheiden (Fall-Chat), Regeln lernen | Klärung-Schnitt · Human First §2 | live (Schnitt) |
| 3 · Abgleichen | Beleg ↔ Buchung matchen; Matcher-Härtung (3 Guards, Transfer-Exclude ↔ neutral R-148) | RECONCILE#01 | Guards gebaut |
| 4 · Abschließen | Periodenabschluss-Lock + Buchhalter-Versand (Vor-Versand-Verifikation) | FES-AP2 | DONE · Echtversand JC-gated |
| 5 · Auswerten | UStVA/EÜR/Cashflow/Kosten — mit Quelle an jeder Zahl (R-91) | FES-AP3 (ustva-prep, C3-Pilot) · KOSTEN#01 | ustva-prep · Cockpits |
| Schicht | Kanon | Stand (ehrlich) |
|---|---|---|
| Datei-Storage | Migration 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-Wahrheit | Jedes 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-Kanon | Ablage-Kanon (Ordner = Default-Tenor, Datei kann strenger — Stufen siehe Sicherheit §3) | Kanon GELOCKT |
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).
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:
| Index | Was er ist | Wo er entsteht |
|---|---|---|
| Vektor (LLM-Chunks) | mistral-embed (EU, 1024 Dim.) → Spalte embedding_mistral; Cosine-kNN über HNSW-Index | skill.py:422f (Embedding-Aufruf) · Mig 196 (HNSW) |
| Wort-Index deutsch | content_tsv, GENERATED-Spalte — füllt sich automatisch beim Insert, kein zweiter Lauf | Mig 072 |
| Wort-Index französisch | content_tsv_french, ebenso GENERATED | Mig 125 |
| Weg | Mechanik | Kennzeichnung 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 Relevanzfilter | weg=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.
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.):
| Regel | Wortlaut |
|---|---|
| Beide immer | Beide 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ührt | Der 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 Beleg | Ausnahme: nur Vektorweg + harter Filter auf genau dieses Dokument (:552–563); die Wortsuche läuft dort bewusst nicht (:582). |
| KAI erfährt die Mischung | Die Trefferzeile (:418–437) sagt KAI wörtlich, wie viele Treffer UNGEPRÜFT sind und dass sie Ergänzung sind, keine Alternative. |
| Nach der Antwort | agent_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). |
| Schwäche | Messung/Beleg |
|---|---|
| Scores trennen nicht | ts_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 Hebel | Mail #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 Vektorweg | p_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 Index | documents.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. |
| Regel | Bedeutung | Anker |
|---|---|---|
| Roh zuerst | save_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 Inhalt | Bill-to/Lieferant aus dem RECHNUNGSINHALT entscheidet — nie die Mail-Adresse/der Kanal | Register (feedback_entity) |
| Triage klassifiziert, Fach-Agent versteht | Nach 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-Code | R-178 · KI-First §4b |
| Stufe reist mit | Vertraulichkeit begleitet das Datum durch JEDEN Schritt (Filing → RAG → Retrieval → Export) — First-Class, kein Add-on | Sicherheit §3 |
| Zwei Datums-Achsen | Eingangsdatum ≠ Rechnungsdatum — beide sichtbar, nie vermischt | JE (Anatomie 05.07.) |
| Schreiben nur mit Freigabe | jede schreibende Fluss-Aktion (buchen, versenden, verschieben) läuft über die Freigabe-Karte + Protokoll | R-94 |
| Der Kreis schließt sich | Jede 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 abschaltbar | KI-First §4d · Register #316/#317 |
| Quelle an jeder Zahl | vom Fluss bis ins Dashboard: #xxxx immer klickbar (GOLDENE REGEL) | R-91 |
| Intervalle prüfen, nicht nur Zeilen | Ein 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 |
| Feld · Achse | Bedeutung | Wo · Füllgrad | gilt für |
|---|---|---|---|
| mail_date_received | Postfach-Eingang (Touch-Once) — WANN kam der Beleg an | documents · 0 % ⛔ Dublette: date_received 86,6 % | jeder Beleg |
| invoice_date | Rechnungsdatum (auf dem Beleg gedruckt) — steuer-/fristrelevant | documents · 62,3 % (das einzige tragende Datum) | Rechnung · Gutschrift · Quittung |
| accounting_period | Buchungsmonat (YYYY-MM). Ableitung: accounting_period → invoice_date → date_received | documents · 2,9 % (fast überall abgeleitet, nicht gesetzt) | alle Buchungs-Typen |
| contract_start_date | ⭐ Vertragsbeginn — DER VERBINDER: ohne ihn kein Kündigungs-Stichtag | prf_contracts · 2,5 % ⛔ der kritische Leerstand | Vertrag · Abo · Versicherung |
| notice_period_days | Kündigungsfrist-DAUER (nicht _months!) — WIE LANG | prf_contracts · 97,1 % ✅ | Vertrag · Abo · Versicherung |
| notice_deadline | Kündigungs-STICHTAG — WANN (= Beginn + Dauer) | prf_contracts · 8,3 % (rechenbar, sobald Beginn da ist) | Vertrag |
| next_renewal_date | nächste automatische Verlängerung (nicht renewal_date!) | prf_contracts · 9,0 % | Vertrag · Abo |
| contract_end_date | Vertragsende | prf_contracts · 9,4 % | Mietvertrag |
| Modell (Quelle) | Person ↔ Firma | Tags / Filter | Einwilligung | Was 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) | keine | Austausch-Format: Export/Import und Handy-Adressbuch. Die DB-Struktur übernehmen wir nicht, denn vCard ist eine Karte, kein Datenmodell. |
| schema.org Person / Organization | worksFor · memberOf · employee | keywords | keine | Vokabular 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 Adresse | Das 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 / Companies | Zuordnungen mit Beschriftung (Primary, „Billing contact“) — eine Person an mehreren Firmen | Listen (statisch/dynamisch) · Lifecycle-Stage | Abo-Typen je Kommunikationszweck | Idee: gespeicherte Filter = dynamische Liste, Zweck-Einwilligung. Deckt sich mit contact_segments + contact_consents.purpose. |
| Punkt | JC hat entschieden | Umsetzung nach Systematik (Reuse, nichts neu) |
|---|---|---|
| PC | Ja zur Liste, mit Detail-Panel rechts wie die anderen Listen-Screens | Das 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-Hinweis | Das Zeichen „von KI ermittelt“ gefällt, bitte als Kanon-Symbol | Das 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). |
| Handy | Variante A: OK | Suche zuerst, Karten, Filter als Bottom-Sheet, tel:/mailto: mit einem Tipp |
| Versand-Vorschau | Vollständigkeit prüfen, keine zweite Mail-Gestalt | Es 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. |
| Art der Mail | Beispiel | Deutschland | Frankreich |
|---|---|---|---|
| ① Geschäftskorrespondenz | Projekt-Abstimmung, Rechnung, Info „neue Kontonummer“, Antwort auf eine Anfrage | frei 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 Angebot | Weihnachtsgruß, Neujahrskarte | zä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 / Newsletter | Seminar-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 Mail | Privatperson: 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) |
| Frage | Worum es geht, mit Beispiel | Empfehlung → 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.) |
| Art | Eingang → Weiche | Erkennung | Ziel · Wand · Dubletten | Wer liest es | Stand |
|---|---|---|---|---|---|
| Bank-CSV | Upload · 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_summary | trägt gelerntes Format nur im Prozess-Speicher (_LLM_FMT_CACHE :527): nach Neustart neu gefragt, nie vom Menschen bestätigt |
| Stunden-CSV | nur Name (timetrack/timesheet/stunden) oder target_stream (dispatch.py:736-739); csv_type='timetrack' wird nicht gelesen | Regeln 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 / Fahrten | kein 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_pendlerpauschale | Erfassen ✅ · Datei-Import ✗ |
| Projektdaten | — | Probe ⇒ unknown / 30 | — (prf_projects schreibt nur project-approve) | — | fehlt |
| Excel (jede Art) | OFFICE_EXT (dispatch.py:458) ⇒ parse-docling | keine: nur .xlsx wird Markdown-Text für die Suche; .xls/.ods ⇒ mime_unsupported | kein Tabellen-Import; ein Excel-Kontoauszug wird auf Sonstiges/UNBEKANNT gesetzt (dispatch.py:679-687) | Suche / RAG | live: 5 .xlsx-Belege, keiner als Tabelle gelesen |
| Unbekannte CSV | csv_triage (dispatch.py:740-742) | unknown / 30, kein _suggestion, keine agent_decisions (classify-csv :686) | bleibt in der Ablage | niemand liest unknown, keine Klärkarte | live: 37 CSV ohne csv_type, 2 unknown |
| Anhang im KAI-Gespräch | derselbe Intake (core_shell.html:24842-24860); der Thread hält den Beleg (app.py:4336-4357) | wie oben | wie oben | KAI sieht nur, was die Pipeline schrieb; er öffnet die Tabelle nicht selbst | keine Analyse als Arbeitsmaterial |
| Klasse 1 · Buchhaltungsrelevant | Klasse 2 · Arbeitsmaterial | |
|---|---|---|
| Beispiele | Kontoauszug · Stundennachweis · Fahrtenbuch/KM · später Rechnungslisten | Projektdaten · Auswertungen · Listen, die KAI im Gespräch rechnen, vergleichen, zeichnen soll |
| Ziel | eine 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) |
| Pflichten | Mandanten-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änderbar | Beleg-Sicht = Mandanten-Wand + Vertraulichkeitsstufe des Belegs, auch in der KAI-Antwort · schreibt nie in Klasse-1-Tabellen |
| Wer entscheidet die Klasse | Der 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. | |