{
  "name": "02_LLM_Pipeline v1.0",
  "nodes": [
    {
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "row_json",
              "type": "any"
            },
            {
              "name": "row_scan_context",
              "type": "any"
            },
            {
              "name": "page_scan",
              "type": "any"
            },
            {
              "name": "page_json",
              "type": "any"
            },
            {
              "name": "year_span",
              "type": "any"
            },
            {
              "name": "_id",
              "type": "any"
            },
            {
              "name": "previous_row_corrected",
              "type": "any"
            },
            {
              "name": "previous_rows_context_string",
              "type": "any"
            },
            {
              "name": "config"
            },
            {
              "name": "validierung_buchstaben"
            },
            {
              "name": "row_scan_magenta"
            },
            {
              "name": "schreiber_beschreibungen",
              "type": "any"
            }
          ]
        }
      },
      "id": "c9c7492d-8ee1-47a2-8355-ea58df70a0c2",
      "typeVersion": 1.1,
      "name": "TRG_Start",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        2736,
        256
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3c080dfa-535e-4bee-b8f6-127b912e6172",
              "name": "rawSystempromptText",
              "value": "==Du bist ein Provenienz-Ermittler f\u00fcr historische Museumsinventare. Deine Aufgabe ist die Rekonstruktion des Erwerbsereignisses aus einem handschriftlichen Inventarbucheintrag. Die Buch-Identifikation und den Sammlungskontext findest du in der Konfiguration im User-Prompt.\n\nDu arbeitest mit bereits extrahierten und GND-validierten Daten. Dein Output sind ausschlie\u00dflich die flachen Provenienz- und Altbestand-Felder im definierten JSON-Schema.\n\nSAMMLUNGSKONTEXT: siehe buch_identifikation und kontext_regeln in der Konfiguration im User-Prompt (geografischer Schwerpunkt, Datumsfenster, Sammlungsschwerpunkt).\n\nGOLD-STANDARD (explizit): Ein Trigger-Wort ist vorhanden (gekauft, geschenkt, remis., \u00fcbernommen, Ankauf, Schenkung, Auktion...) \u2192 vollst\u00e4ndiges Provenienz-Ereignis extrahieren.\n\nSILBER-STANDARD (implizit): Kein Trigger-Wort, aber Indizien vorhanden\n(Name, Datum, Preis in Provenienz-Spalte) \u2192 Indizien extrahieren,\nprovenienz_ereignis_typ MUSS \"<unbekannt>\" bleiben \u2014 auch wenn Preis\nund Name gleichzeitig vorhanden sind. Ohne Trigger-Wort ist kein\nEreignistyp ableitbar.\n\nHARTE VERBOTE:\n- Das Inventarisierungsdatum (eintragsdatum_iso) darf NIEMALS in provenienz_datum_iso_von\n- Objektdatierungen (\"um 1900\", \"18. Jh.\") sind KEINE Provenienz-Daten\n- Bei Unsicherheit: leer lassen statt raten\n- Wenn der Schrift-Validator-Befund (im User-Prompt) einen ZWEIFEL\n  f\u00fcr die Provenienz- oder Preis-Spalte markiert: Wert nur \u00fcbernehmen\n  wenn er durch GND-Entit\u00e4ten oder visuelle Pr\u00fcfung best\u00e4tigt ist.\n- Die Spalte mit role_kuenstler_und_adresse enth\u00e4lt den K\u00fcnstler/Einlieferer \u2014\n  NIEMALS automatisch als Verk\u00e4ufer \u00fcbernehmen ohne explizites Triggerwort.\n  Der K\u00fcnstler ist nicht zwingend der Verk\u00e4ufer.\n\nALTBESTAND-KLASSIFIKATION:\nDie buchspezifischen Altbestand-Hinweise findest du im User-Prompt\nunter kontext_regeln.provenienz_regeln.altbestand_hinweis.\n- Wenn LLM6 audit_funde (im User-Prompt unter SEKUND\u00c4RE INVENTARNUMMERN)\n  Eintr\u00e4ge enth\u00e4lt (audit_funde.length > 0) UND die gefundenen Systeme\n  dem altbestand_hinweis entsprechen\n  \u2192 altbestand_ist: true, kategorie: \"reinventarisierung\"\n- Wenn LLM3 weitere_inventarnummern Eintr\u00e4ge enth\u00e4lt (im User-Prompt\n  unter INVENTARNUMMER) UND die Systeme dem altbestand_hinweis entsprechen\n  \u2192 altbestand_ist: true, kategorie: \"reinventarisierung\"\n- Wenn expliziter Vermerk \"Altbestand\" / \"Alter Besitz\"\n  \u2192 altbestand_ist: true, kategorie: \"explizit\"\n- Sonst \u2192 altbestand_ist: false, kategorie: \"keiner\"\n\nPROVENIENZ-THESAURUS (normierte Hauptbegriffe, Gruppe 1-3):\nAnkauf | Auktion | Schenkung | Schenkung unter Auflage | Verm\u00e4chtnis | Erbe | Tausch | \u00dcbereignung | \u00dcbernahme | Leihgabe | Dauerleihgabe | Verwahrung | Beschlagnahmung_NS_Kontext | Zwangsverkauf_NS_Kontext | Arisierung | Beutekunst | Restitution | \u00dcbernahme_Volkseigentum | Bodenreform | Enteignung_Republikflucht | Sequestration_DDR | Kolonialerwerbung\n\nVARIANTEN \u2192 NORMIERUNG (Beispiele):\n\"angekauft\" \u2192 Ankauf | \"geschenkt\" \u2192 Schenkung | \"remis.\" / \"remittiert\" \u2192 Ankauf | \"\u00fcbernommen\" \u2192 \u00dcbernahme | \"gestiftet\" \u2192 Schenkung | \"testamentarisch\" \u2192 Verm\u00e4chtnis | \"Nachlass\" \u2192 Erbe | \"versteigert\" \u2192 Auktion | \"getauscht\" \u2192 Tausch\n\nW\u00c4HRUNGEN: Die erwarteten W\u00e4hrungen findest du im User-Prompt\nunter kontext_regeln.provenienz_regeln.erwartete_waehrung und waehrung_alternativ.\n\nVISUELLE ANALYSE:\nDu erh\u00e4ltst den Scan der Zeile. Analysiere gezielt:\n- Die Spalte mit der Provenienz-Rolle (gem\u00e4\u00df spalten_zuweisung im User-Prompt)\n- Die Spalte mit der Preis-Rolle (gem\u00e4\u00df spalten_zuweisung im User-Prompt)\n- Randbereiche und Stempel auf Provenienz-Vermerke\n- Rote Schrift / Nachtr\u00e4ge die OCR nicht erfasst hat\n\nAUSGABE: Ausschlie\u00dflich valides JSON-Objekt ohne Markdown, exakt nach folgendem Schema:\n\nAUSGABE-SCHEMA (exakt diese Felder, kein Markdown, kein Freitext):\n{\n  \"provenienz_ereignis_typ\": \"Ankauf | Schenkung | Auktion | ... | <unbekannt>\",\n  \"provenienz_datum_iso_von\": \"YYYY-MM-DD | YYYY-MM | YYYY | <unbekannt>\",\n  \"provenienz_datum_iso_bis\": null,\n  \"provenienz_preis_wert\": 500.00,\n  \"provenienz_preis_waehrung\": \"DM | RM | Mark | <unbekannt>\",\n  \"provenienz_verkaufer_name\": \"Name | <unbekannt> | <nicht_anwendbar>\",\n  \"provenienz_verkaufer_typ\": \"Person | Institution | null\",\n  \"provenienz_verkaufer_gnd_id\": \"GND-ID | null\",\n  \"provenienz_schenkender_name\": \"Name | <unbekannt> | <nicht_anwendbar>\",\n  \"provenienz_schenkender_typ\": \"Person | Institution | null\",\n  \"provenienz_schenkender_gnd_id\": \"GND-ID | null\",\n  \"provenienz_transaktionsort_name\": \"Ort | <unbekannt>\",\n  \"provenienz_transaktionsort_gnd_id\": \"GND-ID | null\",\n  \"provenienz_weitere_akteure_namen\": \"Name1; Name2 | <nicht_anwendbar>\",\n  \"provenienz_begruendung\": \"Kurze Begr\u00fcndung max 30 W\u00f6rter\",\n  \"prov_rekonstruktionstext\": \"Lesbarer Satz f\u00fcr Forscher z.B. Erworben von X f\u00fcr Y DM\",\n  \"prov_vollstaendige_kette\": [{\"datum\": \"...\", \"ereignis\": \"...\", \"akteur\": \"...\"}],\n  \"altbestand_ist\": true,\n  \"altbestand_kategorie\": \"reinventarisierung | explizit | keiner\",\n  \"altbestand_indikatoren\": \"z.B. Sekund\u00e4rnummer aus hist. Nummernkreis (aus Config)\",\n  \"altbestand_confidence\": 0.9\n}\n\nREGELN F\u00dcR NICHT-ANWENDBARE FELDER:\n- Ereignistyp Ankauf \u2192 provenienz_schenkender_* = \"<nicht_anwendbar>\"\n- Ereignistyp Schenkung \u2192 provenienz_verkaufer_* = \"<nicht_anwendbar>\"\n- Keine weiteren Akteure \u2192 provenienz_weitere_akteure_namen = \"<nicht_anwendbar>\"\n- Kein Preis erkennbar \u2192 provenienz_preis_wert = null, provenienz_preis_waehrung = null",
              "type": "string"
            },
            {
              "id": "6f961d5d-fc9f-4aa9-abec-f1c5863e6703",
              "name": "rawPromptText",
              "value": "==Analysiere diesen Inventarbucheintrag auf Provenienz-Informationen.\n\n=== BUCH-KONFIGURATION ===\n{{ $json.Input.config_sektion_zwei }}\n\nBUCHSPEZIFISCHE PROVENIENZ-PARAMETER:\n- Erwartete W\u00e4hrung: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.erwartete_waehrung || 'unbekannt' }}\n- Alternative W\u00e4hrungen: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.waehrung_alternativ || []) }}\n- Altbestand-Hinweis: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.altbestand_hinweis || 'kein Hinweis' }}\n- Datumsfenster: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.datumsfenster_iso || 'unbekannt' }}\n- Datum-Warnung: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.provenienz_regeln?.datum_warnung || '' }}\n\n=== SPALTEN-ZUWEISUNG (DYNAMISCH) ===\n{{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).spalten_zuweisung || {}) }}\n\n=== BEREITS EXTRAHIERTE DATEN (aus vorherigen Pipeline-Schritten) ===\n\nALLE SPALTEN (final):\n{{ JSON.stringify($json['LLM 4'].spalten_vollstaendig_final) }}\n\nINVENTARNUMMER: {{ $json['LLM 3'].inventarnummer.normalisiert }} | Eintragsdatum: {{ $json['LLM 4'].eintrag_metadata.eintrag_datum_iso }}\n\nSEKUND\u00c4RE INVENTARNUMMERN (von LLM6 visuell gefunden):\n{{ JSON.stringify($json['LLM 6'].audit_funde || []) }}\n\nGND-VALIDIERTE ENTIT\u00c4TEN (von LLM5):\n{{ JSON.stringify($json['LLM 5']) }}\n\n=== SCHRIFT-VALIDATOR BEFUND ===\n{{ JSON.stringify($json.Input.validierung) }}\n\nDer Befund markiert Stellen wo Transkribus m\u00f6glicherweise\nfalsch gelesen hat. F\u00fcr deine Provenienz-Analyse:\n- Identifiziere Provenienz- und Preis-Spalte \u00fcber spalten_zuweisung oben\n- Wenn diese Spalten einen ZWEIFEL tragen: erh\u00f6hte Vorsicht \u2014\n  im Zweifel leer lassen statt falschen Wert \u00fcbernehmen\n- F\u00fcr Namen: bevorzuge den GND-validierten official_name\n  aus LLM5 gegen\u00fcber dem Spaltentext\n\n=== AUFGABE ===\n\nLies die spalten_zuweisung aus der Buch-Konfiguration.\nIdentifiziere welche Spalte die Provenienz-Rolle und welche die Preis-Rolle hat.\nF\u00fchre dann die dreistufige Provenienz-Analyse durch:\n\n1. Prim\u00e4r-Analyse: Provenienz-Spalte + Preis-Spalte auf Gold/Silber-Standard pr\u00fcfen\n2. Sekund\u00e4r-Analyse: alle anderen Spalten auf zus\u00e4tzliche Indizien\n3. Synthese: zu einem koh\u00e4renten Provenienz-Ereignis zusammenf\u00fchren\n\nNutze die GND-validierten Personen/Institutionen f\u00fcr die Rollen-Zuordnung.\nBeachte das visuelle Bild \u2014 rote Nachtr\u00e4ge und Stempel k\u00f6nnen Provenienz-Vermerke enthalten die im OCR fehlen.\n\nAntworte ausschlie\u00dflich mit dem JSON-Objekt gem\u00e4\u00df Output-Schema.",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "2b98afb5-8583-48e6-a450-cbfb3ba552a7",
      "name": "SET_LLM7_Prompt",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2976,
        3616
      ]
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_LLM7_Provenienz | v1.0 \u2550\u2550\u2550\n// Zweck: Baut LLM7-Request (Provenienz, qwen) inkl. Bild\n\n// LLM7: Provenienz-Extraktion (Vision + Text) \u2014 qwen3.6-27b-mtp\n// Dense-Modell f\u00fcr pr\u00e4zise strukturierte Extraktion mit Vision.\n// Nutzt vorberechneten base64 aus FN_Load_Images \u2014 kein erneuter Download.\nconst item = $input.item.json;\n// FIX Bug 1: Bilder direkt aus 'Input Images1' beziehen (Set-Nodes schleifen sie nicht durch)\nconst __imgSrc = $('FN_Load_Images').item.json;\nif (!item._row_scan_magenta_data_url) item._row_scan_magenta_data_url = __imgSrc._row_scan_magenta_data_url;\nif (!item._context_scan_data_url) item._context_scan_data_url = __imgSrc._context_scan_data_url;\n\nif (!item.rawPromptText || !item.rawSystempromptText) {\n  throw new Error('Input-Felder unvollst\u00e4ndig: rawPromptText oder rawSystempromptText fehlt!');\n}\n\nconst userContent = [{ type: 'text', text: item.rawPromptText }];\n\n// Vision: row_scan_magenta als prim\u00e4res Bild (Spalten-Grenzen sichtbar)\nif (item._row_scan_magenta_data_url) {\n  userContent.push({ type: 'text', text: '\\n### VISUELLER SCAN (mit Spalten-Grenzen):\\nDie magentafarbenen Linien zeigen Spalten-Grenzen \u2014 KEIN inhaltlicher Bestandteil. Analysiere gezielt Spalte 6 (Provenienz) und Spalte 7 (Preis) sowie Randbereiche auf rote Nachtr\u00e4ge und Stempel:' });\n  userContent.push({ type: 'image_url', image_url: { url: item._row_scan_magenta_data_url } });\n}\n\nreturn {\n  json: {\n    model: 'qwen3.6-27b-mtp',\n    response_format: { type: 'json_object' },\n    messages: [\n      { role: 'system', content: item.rawSystempromptText },\n      { role: 'user',   content: userContent }\n    ],\n    temperature: 0.1,\n    top_p: 0.8,\n    top_k: 20,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 4096,\n    reasoning_budget: 2048,\n    chat_template_kwargs: { enable_thinking: false }\n  }\n};"
      },
      "id": "8221ab71-ab51-45de-afc3-04a2c573cb7b",
      "name": "REQ_LLM7_Provenienz",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3200,
        3616
      ]
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_LLM7 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM7-Response \u2192 'LLM 7'-Objekt\n\n// LLM7 Response \u2192 als 'LLM 7' Objekt in die Pipeline-Struktur einbauen\nconst item = $input.item.json;\n\nlet raw = item.choices?.[0]?.message?.content || '{}';\nif (typeof raw !== 'string') raw = JSON.stringify(raw);\n\nlet parsed = {};\ntry {\n  const clean = raw.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch(e) {\n  parsed = {};\n  item._error = true; item._error_source = 'Parser LLM 7'; // FIX Bug 6\n}\n\nreturn {\n  json: {\n    ...item,\n    'LLM 7': {\n      provenienz_ereignis_typ:        parsed.provenienz_ereignis_typ ?? '',\n      provenienz_datum_iso_von:        parsed.provenienz_datum_iso_von ?? '',\n      provenienz_datum_iso_bis:        parsed.provenienz_datum_iso_bis ?? null,\n      provenienz_preis_wert:           parsed.provenienz_preis_wert ?? null,\n      provenienz_preis_waehrung:       parsed.provenienz_preis_waehrung ?? '',\n      provenienz_verkaufer_name:       parsed.provenienz_verkaufer_name ?? '',\n      provenienz_verkaufer_typ:        parsed.provenienz_verkaufer_typ ?? null,\n      provenienz_verkaufer_gnd_id:     parsed.provenienz_verkaufer_gnd_id ?? null,\n      provenienz_schenkender_name:     parsed.provenienz_schenkender_name ?? '',\n      provenienz_schenkender_typ:      parsed.provenienz_schenkender_typ ?? null,\n      provenienz_schenkender_gnd_id:   parsed.provenienz_schenkender_gnd_id ?? null,\n      provenienz_transaktionsort_name: parsed.provenienz_transaktionsort_name ?? '',\n      provenienz_transaktionsort_gnd_id: parsed.provenienz_transaktionsort_gnd_id ?? null,\n      provenienz_weitere_akteure_namen: parsed.provenienz_weitere_akteure_namen ?? '',\n      provenienz_begruendung:          parsed.provenienz_begruendung ?? '',\n      prov_rekonstruktionstext:        parsed.prov_rekonstruktionstext ?? '',\n      prov_vollstaendige_kette:        parsed.prov_vollstaendige_kette ?? [],\n      altbestand_ist:                  parsed.altbestand_ist ?? false,\n      altbestand_kategorie:            parsed.altbestand_kategorie ?? 'keiner',\n      altbestand_indikatoren:          parsed.altbestand_indikatoren ?? '',\n      altbestand_confidence:           parsed.altbestand_confidence ?? null,\n    },\n    llm7_tokens: item.usage?.total_tokens ?? 0\n  }\n};"
      },
      "id": "7d422c4e-f21f-456f-8af9-42011371ca6a",
      "name": "PARSE_LLM7",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3648,
        3616
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "======================================================================\nUSER-INPUT: AKTUELLE KONFIGURATION & ZEILENDATEN\n=====================================================================\n### 1. BUCH-KONFIGURATION (DEIN GESETZ)\n{{ $('SET_Input_Row').item.json.config_sektion_zwei }}\n\n### 2. ROHDATEN DER AKTUELLEN ZEILE\n- Spalte 1: {{ $('SET_Input_Row').item.json.row_spalte_1 }}\n- Spalte 2: {{ $('SET_Input_Row').item.json.row_spalte_2 }}\n- Spalte 3: {{ $('SET_Input_Row').item.json.row_spalte_3 }}\n- Spalte 4: {{ $('SET_Input_Row').item.json.row_spalte_4 }}\n- Spalte 5: {{ $('SET_Input_Row').item.json.row_spalte_5 }}\n- Spalte 6: {{ $('SET_Input_Row').item.json.row_spalte_6 }}\n- Spalte 7: {{ $('SET_Input_Row').item.json.row_spalte_7 }}\n- Spalte 8: {{ $('SET_Input_Row').item.json.row_spalte_8 }}\n- Spalte 9: {{ $('SET_Input_Row').item.json.row_spalte_9 }}\n- Spalte 10: {{ $('SET_Input_Row').item.json.row_spalte_10 }}\n\n### 3. VISUELLE REFERENZ (BILD-ANALYSE)\nDie zugeh\u00f6rigen Scans (Detail-Scan der Zeile und der erweiterte Kontext-Scan) sind dieser Nachricht als direkte Bildanh\u00e4nge unten beigef\u00fcgt. Gleiche das Bild zwingend mit den Rohdaten aus Sektion 2 ab (Achte besonders auf DITO-Zeichen, Rote Tinte und Streichungen).\n\n### 4. DEIN AUFTRAG\nAnalysiere die Daten und die angeh\u00e4ngten Bilder. Nutze die Regeln des System-Prompts.\nGib das Ergebnis exakt in diesem JSON-Schema zur\u00fcck.\nWICHTIG: GIB NUR DAS NACKTE JSON AUS. KEIN MARKDOWN!\n\n{\n  \"zeilen_klassifikation\": {\n    \"typ\": \"NORMAL | FORTSETZUNG | SAMMELZEILE | DURCHGESTRICHEN | IGNORIEREN\",\n    \"begruendung\": \"Warum hast du so entschieden?\",\n    \"visuelle_marker\": [\"ROT\", \"DITO\", \"STREICHUNG\"]\n  },\n  \"eintrag_durchgestrichen\": false,\n  \"row_id\": \"{{ $('SET_Input_Row').item.json._id }}\",\n  \"_debug_llm1\": {\n    \"fehler_flags\": [\n      \"UNKLARE_KLASSIFIKATION\",\n      \"KEIN_ANKER_GEFUNDEN\",\n      \"REGELKONFLIKT_SYMBOLE\"\n    ],\n    \"fehler_details\": [\n      \"Konflikt: ...\"\n    ],\n    \"unsicherheit_score\": 0.3,\n    \"hinweise\": [\n      \"...\"\n    ]\n  }\n}",
              "type": "string"
            },
            {
              "id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
              "name": "rawSystempromptText",
              "value": "======================================================================\nARCHIV-LLM - SYSTEM: AGNOSTISCHER KLASSIFIZIERUNGS-AGENT\n=====================================================================\n\nI. IDENTIT\u00c4T & AUFTRAG\nDu bist ein hochspezialisierter Archivar f\u00fcr die digitale Erschlie\u00dfung historischer Inventarb\u00fccher.\nDeine einzige Aufgabe ist die pr\u00e4zise Klassifizierung einer Tabellenzeile in einen der f\u00fcnf definierten Typen. Nutze daf\u00fcr die gelieferten Textdaten und die angeh\u00e4ngten Bild-Scans.\n\nII. DIE DOKTRIN DER AGNOSTIK (WICHTIG!)\nDu triffst keine Annahmen \u00fcber das Buch. Dein gesamtes Wissen \u00fcber Spalten, Rollen und Symbole beziehst du ausschlie\u00dflich aus der im User-Prompt \u00fcbergebenen 'buch_konfiguration'.\n- Identifiziere zuerst die ANKER-SPALTE: die Spalte, deren Rollenbeschreibung 'role_primaer_inventarnummer' enth\u00e4lt.\n- Identifiziere die DITO-SYMBOLE ausschlie\u00dflich aus: kontext_regeln.wiederholungs_logik.symbole.\n- Wenn in der Config zus\u00e4tzlich eine Entf\u00e4llt-/Literal-Symbol-Logik existiert (z.B. kontext_regeln.entfaellt_logik.symbole), dann sind diese Symbole als sichtbarer Inhalt zu behandeln (niemals \"leer\").\n\nIII. SYMBOL- & LEERHEITSREGELN (HART)\n1) DITO ist NUR das, was explizit in kontext_regeln.wiederholungs_logik.symbole steht.\n   - Alle anderen Symbole sind NICHT DITO, auch wenn sie wie Platzhalter wirken.\n2) Sichtbarer Inhalt darf niemals als \"leer\" interpretiert werden.\n   - Wenn eine Zelle irgendein Zeichen enth\u00e4lt (z.B. ein Symbol, Zahlen oder Buchstaben), ist sie NICHT leer.\n3) \"Leer\" bedeutet ausschlie\u00dflich: null oder \"\" (keine Zeichen).\n4) Keine Schema-Erfindung:\n   - Ignoriere die Existenz hypothetischer zus\u00e4tzlicher Spalten (z.B. spalte_10). Klassifiziere nur anhand der vorhandenen Zellinhalte und der Config-Rollen.\n\nIV. KLASSIFIZIERUNGS-HIERARCHIE (PRIORISIERT)\nPr\u00fcfe die Zeile in exakt dieser Reihenfolge:\n\n1. DURCHGESTRICHEN (Priorit\u00e4t 1):\n   - Visuelle Merkmale: Eine physische Linie verl\u00e4uft DURCH den Textk\u00f6rper \n     der Anker-Spalte oder der Hauptzeile.\n   - Textliche Merkmale: \"gestrichen\", \"storniert\", \"ung\u00fcltig\", \"gel\u00f6scht\" \n     oder ein rotes \"X\" das den Text kreuzt.\n   - PR\u00c4ZISIERUNG ROTE TINTE (KRITISCH):\n     \u2192 Rote Tinte NUR dann als Streichung werten, wenn eine physische Linie\n       den Textk\u00f6rper kreuzt.\n     \u2192 Rote Tinte NEBEN, UNTER oder \u00dcBER dem Text ist ein Nachtrag oder\n       Verwaltungsvermerk \u2013 KEIN Streichungsindikator.\n     \u2192 Beispiel KEIN Streichung: roter Datumsvermerk \"3.8.70\" neben einer\n       Zelle, roter Stempel, rote Kontrollnummer.\n     \u2192 Beispiel STREICHUNG: rote Linie horizontal durch einen Texteintrag.\n\n2. FORTSETZUNG (Priorit\u00e4t 2):\n   - Die ANKER-SPALTE ist leer (null/\"\") ODER enth\u00e4lt ausschlie\u00dflich ein DITO-SYMBOL.\n   - UND es existiert in mindestens einer anderen Spalte sichtbarer, nicht-leerer Inhalt (Text, Zahl ODER ein beliebiges sichtbares Symbol).\n   - Hinweis: Auch reine Symbol-/Status-/Entf\u00e4llt-Inhalte (sofern sichtbar) z\u00e4hlen als Inhalt und machen die Zeile zur FORTSETZUNG (nicht IGNORIEREN).\n\n3. SAMMELZEILE (Priorit\u00e4t 3):\n   - Die ANKER-SPALTE enth\u00e4lt einen Bereich (z.B. \"XI 1-10\", \"Nr. 15 bis 20\", \"23-28\", \"XII/23\u201328\").\n   - Textsignale wie \"Serie\", \"Konvolut\", \"Sammlung\", \"Konv.\", \"Gruppe\".\n\n4. NORMAL (Priorit\u00e4t 4):\n   - Die ANKER-SPALTE enth\u00e4lt eine neue, valide Inventarnummer (z.B. eine valide Prim\u00e4rnummer gem\u00e4\u00df primaer_inventarnummer_regex aus der Konfiguration).\n   - Auch bei OCR-Fehlern (z.B. \"X1\", \"11\", \"HI\" statt \"XI\") ist dies als NORMAL einzustufen, wenn das Muster erkennbar ist.\n   - WICHTIG: Du \"reparierst\" nichts; du klassifizierst nur.\n\n5. IGNORIEREN:\n   - Die Zeile ist komplett leer (alle Zellen null/\"\") oder enth\u00e4lt nur unleserliches Bildrauschen ohne interpretierbare Zeichen.\n\nV. DEBUGGING-PFLICHT (WICHTIG)\nF\u00fclle das Feld \"_debug_llm1\" NUR dann, wenn mindestens einer dieser F\u00e4lle eintritt:\n- UNKLARE_KLASSIFIKATION: Bedingungen f\u00fcr zwei oder mehr Typen sind erf\u00fcllt.\n- KEIN_ANKER_GEFUNDEN: Config liefert keine Prim\u00e4rinventarnummer-Spalte.\n- REGELKONFLIKT_SYMBOLE: DITO vs. Entf\u00e4llt vs. sichtbarer Inhalt steht in Konflikt.\nWenn alles klar ist: setze \"_debug_llm1\" auf ein leeres Objekt {}.\n\nVI. OUTPUT-FORMAT (ABSOLUTES GESETZ)\nAntworte AUSSCHLIESSLICH im puren JSON-Format gem\u00e4\u00df dem geforderten Schema.\nVerwende KEINERLEI Markdown-Formatierung (kein ```json ... ```). \nGeneriere keinen Begleittext vor oder nach dem JSON-Objekt.\n\nDOPPELKLASSIFIKATION: Eine Zeile kann gleichzeitig DURCHGESTRICHEN und FORTSETZUNG sein. Beide classification_typ-Werte d\u00fcrfen gleichzeitig gesetzt werden \u2014 z.B. classification_typ: ['DURCHGESTRICHEN', 'FORTSETZUNG']. Dies ist kein Fehler, sondern ein valider Output bei Zeilen die als Fortsetzung markiert und zus\u00e4tzlich durchgestrichen wurden.",
              "type": "string"
            },
            {
              "id": "e330cf53-5f13-4a48-b607-72052276d414",
              "name": "row_scan_context",
              "value": "={{ $('SET_Input_Row').item.json.row_scan_context }}",
              "type": "string"
            },
            {
              "id": "d992aa74-1812-49fd-8c1a-c1674cf772bc",
              "name": "page_scan",
              "value": "={{ $('SET_Input_Row').item.json.page_scan }}",
              "type": "string"
            },
            {
              "id": "bc119282-b1bd-4d2a-957a-7f0005c86ce9",
              "name": "page_scan_minus_1",
              "value": "={{ $('SET_Input_Row').item.json.page_scan_minus_1 }}",
              "type": "string"
            },
            {
              "id": "9fd00e7f-86a7-4171-86df-fa149bce1adc",
              "name": "page_scan_minus_2",
              "value": "={{ $('SET_Input_Row').item.json.page_scan_minus_2 }}",
              "type": "string"
            },
            {
              "id": "9ec316ce-f07e-463f-bfa8-60d745989137",
              "name": "page_scan_minus_3",
              "value": "={{ $('SET_Input_Row').item.json.page_scan_minus_3 }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2960,
        496
      ],
      "id": "d06f27d2-e462-403d-9b9b-3cff22da1472",
      "name": "SET_LLM1_Prompt"
    },
    {
      "parameters": {
        "url": "={{ $json.row_json.startsWith('http') ? $json.row_json : 'https://' + $json.row_json.replace(/^[\\\\[\\\\]]/g,'') }}\n",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3184,
        256
      ],
      "id": "c45cb324-8cc6-43eb-a830-7301d17ac468",
      "name": "HTTP_Fetch_RowJson",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "url": "={{ $('SET_Input_Init').item.json.page_json.startsWith('http') ? $('SET_Input_Init').item.json.page_json : 'https://' + $('SET_Input_Init').item.json.page_json.replace(/^[\\\\[\\\\]]/g,'') }}",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {}
          ]
        },
        "options": {
          "response": {
            "response": {}
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3408,
        256
      ],
      "id": "c3c250ae-b826-466b-9ab3-47e2344b02bb",
      "name": "HTTP_Fetch_PageJson"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ea74a855-51c3-4b70-bd00-c359d1dc916a",
              "name": "row_json",
              "value": "={{ $json.row_json }}",
              "type": "string"
            },
            {
              "id": "21efd414-eed5-41e3-b8de-1cc73be2f502",
              "name": "row_scan_context",
              "value": "={{ $json.row_scan_context }}",
              "type": "string"
            },
            {
              "id": "763e5362-64b9-4364-acaf-ea37c6314657",
              "name": "page_scan",
              "value": "={{ $json.page_scan }}",
              "type": "string"
            },
            {
              "id": "86f30f5e-d453-4dec-b012-4b3c386e718f",
              "name": "page_json",
              "value": "={{ $json.page_json }}",
              "type": "string"
            },
            {
              "id": "142f1f10-df23-4ae6-8192-62bc9bf98520",
              "name": "year_span",
              "value": "={{ $json.year_span }}",
              "type": "string"
            },
            {
              "id": "6bedfacb-b805-4166-b1ba-152bf22b7b38",
              "name": "_id",
              "value": "={{ $json._id }}",
              "type": "string"
            },
            {
              "id": "b8808dcb-ec85-429f-85f5-9de7e668de4c",
              "name": "currentItem.previous_row_corrected",
              "value": "={{ $json.previous_row_corrected }}",
              "type": "string"
            },
            {
              "id": "09255033-62b3-414d-9de2-6b87cbf41e43",
              "name": "previous_rows_context_string",
              "value": "={{ $json.previous_rows_context_string }}",
              "type": "string"
            },
            {
              "id": "5c06f4ad-d825-48a1-aed2-380dbd82ffe4",
              "name": "config",
              "value": "={{ $json.config }}",
              "type": "string"
            },
            {
              "id": "1a56a991-d79c-4670-afe4-3ddee8c22604",
              "name": "validierung",
              "value": "={{ $json.validierung_buchstaben }}",
              "type": "string"
            },
            {
              "id": "3ecc1539-1104-49de-9e19-e86149cf793b",
              "name": "row_scan_magenta",
              "value": "={{ $json.row_scan_magenta }}",
              "type": "string"
            },
            {
              "id": "init_schreiber_beschr",
              "name": "schreiber_beschreibungen",
              "value": "={{ $json.schreiber_beschreibungen }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2960,
        256
      ],
      "id": "a3b83e25-267a-486e-8dd4-3b25d73e8575",
      "name": "SET_Input_Init"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "======================================================================\nUSER-INPUT: SEQUENZ-DATEN & BUCH-KONFIGURATION (HYBRID)\n=====================================================================\n\n### 1. DIE DATEN (FAKTEN)\nA) AKTUELLE ZEILE (Cleaned Data)\n- Rohtexte: {{ JSON.stringify($json['LLM 2'].cleaned_spalten) }}\n- Klassifizierung: {{ $json['LLM 1'].zeilen_klassifikation.typ }}\n- Durchgestrichen: {{ $json['LLM 1'].eintrag_durchgestrichen }}\n\nB) VORZEILE (Nur f\u00fcr Audit - NIEMALS ERBEN!)\n- Kontext: {{ JSON.stringify($json.Input.currentItem.previous_row_corrected) }}\n- InvNr Normalisiert: {{ $json.Input.currentItem.previous_row_corrected.inv_nr_normalisiert ?? null }}\n\nC) VISUELLE REFERENZ (BILD-ANALYSE)\nDie zugeh\u00f6rigen Scans (Detail-Scan der Zeile und der erweiterte Kontext-Scan) sind dieser Nachricht als direkte Bildanh\u00e4nge unten beigef\u00fcgt. Gleiche das Bild zwingend mit den OCR-Rohdaten ab. Achte dabei besonders auf historisch korrekte Leseweisen, Streichungen oder unklare Trennzeichen bei den Nummern.\n\n---\n### 1b. VORAB-BEFUND: SCHRIFT-VALIDATOR\nQuelle: Spezialisiertes Vision-System, unabh\u00e4ngig von Transkribus.\nMethode: Zeichenweiser Vergleich des Zeilenscans gegen eine\n         schreiberspezifische Buchstabentafel.\n\n{{ JSON.stringify($json.Input.validierung) }}\n\nHinweis zur Nutzung:\n- BESTAETIGT = kein visueller Zweifel an diesem Zeichen\n- ZWEIFEL / STARK_ABWEICHEND = das Zeichen sieht im Bild\n  anders aus als Transkribus es gelesen hat\n- Der Befund ist ein Indiz, kein Urteil \u2014 du entscheidest.\n\n\n---\n### 2. DIE BUCH-REGELN (DYNAMISCH)\n{{ $json.Input.config_sektion_zwei }}\n\nKONKRETE PARAMETER F\u00dcR DIESES BUCH:\n- Regex f\u00fcr Prim\u00e4rnummer (Gesetz): {{ JSON.parse($json.Input.config_sektion_zwei).cleaning_rules?.spalte_1?.regex_validation || \"KEINE REGEX - Nutze generische Plausibilit\u00e4t\" }}\n- Leerzeichen entfernen: {{ JSON.parse($json.Input.config_sektion_zwei).cleaning_rules?.spalte_1?.remove_spaces ? \"JA\" : \"NEIN\" }}\n- Zus\u00e4tzliche Suchmuster (aus Config): {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).sonderfaelle_und_annotationen?.kontrollnummern_muster || []) }}\n- Sekund\u00e4re Nummernkreise dieses Hauses: {{ (function() { const sys = JSON.parse($json.Input.config_sektion_zwei).sekundaer_nummernkreise || []; if (!sys.length) return 'KEINE DEFINIERTEN NUMMERNKREISE \u2014 nur Regex-Muster aus Zus\u00e4tzliche Suchmuster verwenden.'; return sys.map((s, i) => (i+1) + '. ' + s.name + ': ' + s.muster + (s.beispiel ? ' (Bsp: ' + s.beispiel + ')' : '')).join('\\n'); })() }}\n- Stempel-Check aktiv: {{ JSON.parse($json.Input.config_sektion_zwei).sonderfaelle_und_annotationen?.vermoegenskartei_stempel_check ? \"JA\" : \"NEIN\" }}\n\n---\n### 3. DEIN AUFTRAG\nA) PRIM\u00c4RNUMMER (STRIKT NACH REGEX)\n- Bestimme die ANKER-SPALTE aus der Config (Suche \"role_primaer_inventarnummer\").\n- Extrahiere den Wert.\n- Validiere gegen die oben genannte REGEX.\n- Wenn kein Match -> `normalisiert` = \"\".\n\nB) SEQUENZ (NUR AUDIT)\n- Vergleiche mit Vorzeile. Berechne Erwartungswert (+1).\n- Dokumentiere Abweichungen nur im Text.\n- NIEMALS die Nummer \u00e4ndern.\n\nC) SEKUND\u00c4RNUMMERN (FORENSIK)\n- Nutze deine INTERNE LISTE bekannter Systeme.\n- Nutze ZUS\u00c4TZLICH die \"Zus\u00e4tzliche Suchmuster\" von oben.\n- Suche in ALLEN Spalten.\n\nD) STEMPEL\n- Wenn aktiv: Suche nach \"Verm\u00f6gensverzeichniskartei\".",
              "type": "string"
            },
            {
              "id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
              "name": "rawSystempromptText",
              "value": "======================================================================\nARCHIV-LLM \u2014 SEQUENZ-VALIDATOR & FORENSIKER (AGNOSTISCH)\n=====================================================================\n\nI. IDENTIT\u00c4T & AUFTRAG\nDu bist der \u201eMathematische Revisor\".\n1) Prim\u00e4r-Inventarnummer extrahieren, reparieren und STRIKT nach Config-Regex validieren.\n2) Sekund\u00e4rnummern anhand deiner INTERNEN LISTE + Config-Hinweisen finden.\n\n---------------------------------------------------------------------\nII. INPUT-DOKTRIN \n- Prim\u00e4rnummer-Regeln (Regex, Reinigungsregeln): Kommen DYNAMISCH als JSON-Parameter aus dem User-Prompt.\n- Sekund\u00e4rnummer-Wissen: Kommt aus deiner INTERNEN LISTE (siehe unten V.) + Erg\u00e4nzungen im Prompt.\n- Visuelle Belege (Scans): Du erh\u00e4ltst zus\u00e4tzlich Bilder des Originaldokuments. Nutze diese zwingend als \"Ground Truth\" (Wahrheit), falls die OCR-Texte fehlerhaft, verrutscht oder unklar sind.\n\n\n---------------------------------------------------------------------\nII.b SCHRIFT-VALIDATOR INPUT\nWenn im User-Prompt ein Vorab-Befund des Schrift-Validators vorliegt:\n\n1. Identifiziere die Spalte mit \"role_primaer_inventarnummer\" aus der Config.\n2. Pr\u00fcfe ob f\u00fcr diese Spalte ein ZWEIFEL oder STARK_ABWEICHEND markiert ist.\n3. Wenn JA und Regex-Mismatch:\n   Nutze zeichen_alternativ als zus\u00e4tzlichen Hinweis f\u00fcr einen\n   Korrektur-Kandidaten. Erh\u00f6he confidence um max. 0.2.\n   Dokumentiere in normalisierungsschritte:\n   \"Schrift-Validator: [hinweis aus dem Befund]\"\n4. Wenn JA aber Regex-Match: Ignoriere den Befund \u2014 Transkribus hat recht.\n5. Wenn BESTAETIGT oder kein Befund vorhanden: Kein Einfluss.\n\n---------------------------------------------------------------------\nIII. HARTREGELN (ANTI-DRIFT)\n1) NO-INHERIT: Du \u00fcbernimmst NIEMALS die Nummer der Vorzeile.\n2) NO-GUESS: Passt die Nummer (auch nach Reparatur) NICHT zur Regex \u2192\n   normalisiert = \"\" \u2014 ES SEI DENN du kannst den Fehler auf einen\n   konkreten visuellen oder sequenziellen Beleg zur\u00fcckf\u00fchren.\n   In diesem Fall: Kandidat mit confidence \u2264 0.3 und PRIMAER_NUMMER_UNKLAR.\n3) ANTI-DUP: Ist die Nummer IDENTISCH zur Vorzeile -> `normalisiert` = \"\" (Verdacht auf Fortsetzung).\n\n---------------------------------------------------------------------\nIV. PRIM\u00c4R-INVENTARNUMMER (EXTRAKTION & REPARATUR)\n1. Identifiziere Anker-Spalte (via Config im User-Prompt, suche nach \"role_primaer_inventarnummer\").\n2. Hole den Rohwert aus dieser Spalte.\n3. F\u00fchre VOR der Regex-Pr\u00fcfung ZWINGEND die im User-Prompt \u00fcbergebenen OCR-Reparaturen durch (siehe Parameter f\u00fcr \"INVENTARNUMMER\"):\n   - Entferne Klammern (falls `remove_brackets` true).\n   - Schneide historische Marker/Haken am Ende ab (siehe `strip_trailing_markers`).\n   - Fixe Trennzeichen-Fehler der OCR (siehe `fix_separators`).\n   - Entferne alle Leerzeichen (falls `remove_spaces` true).\n4. Pr\u00fcfe den reparierten Wert gegen die \u00fcbergebene `regex_validation`.\n   - MATCH: Wert in `normalisiert` \u00fcbernehmen.\n   - NO MATCH: Pr\u00fcfe zun\u00e4chst ob du aus dem Kontext einen plausiblen Kandidaten\n     ableiten kannst:\n     a) Steht im Bild eine Nummer die zur Regex passt und die der OCR-Rohwert\n        erkl\u00e4rbar verf\u00e4lscht hat? (z.B. \"XI/1\" wurde als \"2/1\" gelesen,\n        \"V1/3\" als \"Y1/3\", \"IV/5\" als \"1V/5\")\n     b) Ergibt sich aus der Sequenz (Vorzeile + 1) ein Kandidat der zur Regex passt?\n     Wenn JA: Setze normalisiert = Kandidat, confidence \u2264 0.3, und dokumentiere\n     in normalisierungsschritte: \"OCR-Korrektur-Kandidat: [original] \u2192 [kandidat],\n     visuell pr\u00fcfen\". Setze PRIMAER_NUMMER_UNKLAR in fehler_flags.\n     Wenn NEIN: normalisiert = \"\".\n5. Falls die Anker-Spalte unplausibel ist: Pr\u00fcfe das angeh\u00e4ngte Bild der Zeile, um die Nummer visuell zu verifizieren.\n\n---------------------------------------------------------------------\nV. FORENSIK: SEKUND\u00c4R-INVENTARNUMMERN (AUS KONFIGURATION)\nScanne ALLE Spalten. Die hausspecifischen Nummernkreise stehen im User-Prompt\nunter \"Sekund\u00e4re Nummernkreise dieses Hauses\" \u2014 verwende ausschlie\u00dflich diese.\nKombiniere sie zus\u00e4tzlich mit den \"Zus\u00e4tzliche Suchmuster\" aus der Config.\nWenn keine Nummernkreise definiert sind: nur Regex-Muster verwenden.\n\nGib jeden Fund im Array `weitere_inventarnummern` aus.\n\n---------------------------------------------------------------------\nVI. STEMPEL-CHECK\nWenn im User-Input \"Stempel-Check aktiv: JA\" steht:\nSuche nach \"Verm\u00f6gensverzeichniskartei\" (oder Fragmenten).\n\n---------------------------------------------------------------------\nVII. OUTPUT SCHEMA (JSON)\n{\n  \"inventarnummer\": {\n    \"original\": \"...\",\n    \"normalisiert\": \"...\",\n    \"zusatz_informationen\": \"\",\n    \"weitere_inventarnummern\": [\n      { \"nummer\": \"...\", \"system\": \"...\", \"fundort_spalte\": \"...\" }\n    ],\n    \"normalisierungsschritte\": [],\n    \"confidence\": 0.0,\n    \"begruendung\": \"...\"\n  },\n  \"eintrag_durchgestrichen\": false,\n\n  \"_debug_llm3\": {\n    \"fehler_flags\": [\n      \"PRIMAER_NUMMER_UNKLAR\",\n      \"SEQUENZKONFLIKT\",\n      \"SEKUNDAER_UNSICHER\",\n      \"KEINE_REGEX_DEFINIERT\"\n    ],\n    \"fehler_details\": [\n      \"Regex fehlt oder ist 'KEINE REGEX - Nutze generische Plausibilit\u00e4t'.\",\n      \"Mehrere plausible Prim\u00e4rnummer-Kandidaten in der Anker-Spalte.\",\n      \"Sequenzabweichung zum Erwartungswert +1.\",\n      \"Sekund\u00e4rnummern-Kandidaten lassen sich keinem bekannten System eindeutig zuordnen.\"\n    ],\n    \"unsicherheit_score\": 0.3,\n    \"hinweise\": [\n      \"Bitte menschlich pr\u00fcfen: Prim\u00e4rnummer vs. Vorzeile und Regex-Konfiguration.\",\n      \"Sekund\u00e4rinventarnummern nur mit Vorsicht \u00fcbernehmen.\"\n    ]\n  }\n}\n\n---------------------------------------------------------------------\nVIII. DEBUGGING-PFLICHT (OPTIONAL, ABER STRUKTURIERT)\n\n1) DEIN ZWEITER AUFTRAG \u2013 INVENTARNUMMER-KONFLIKTE SICHTBAR MACHEN\nNeben der Extraktion und Validierung der Inventarnummern musst du jede\ninhaltliche Unsicherheit oder jeden Regelkonflikt EXPLIZIT im Feld \"_debug_llm3\"\ndokumentieren.\n\n2) WANN \"_debug_llm3\" GEF\u00dcLLT WERDEN SOLL\nSetze passende Fehler-Flags, wenn mindestens einer der folgenden F\u00e4lle auftritt:\n\n- KEINE_REGEX_DEFINIERT\n  Die Buch-Konfiguration enth\u00e4lt keine valide Regex f\u00fcr die Prim\u00e4rnummer.\n\n- PRIMAER_NUMMER_UNKLAR\n  Kein Kandidat der nach Reparatur zur Regex passt (normalisiert bleibt leer),\n  ODER ein OCR-Korrektur-Kandidat wurde gesetzt (confidence \u2264 0.3).\n\n- SEQUENZKONFLIKT\n  Die aktuelle normalisierte Prim\u00e4rnummer weicht vom erwarteten Sequenzwert\n  (+1 gegen\u00fcber der Vorzeile) ab. Du \u00e4nderst die Nummer NICHT, sondern\n  dokumentierst nur.\n\n- SEKUNDAER_UNSICHER\n  Gefundene Sekund\u00e4rnummern passen zwar teilweise zu Mustern, lassen sich aber\n  keinem System eindeutig zuordnen oder wirken widerspr\u00fcchlich.\n\n3) FELDER IM DEBUG-OBJEKT\nWenn mindestens ein Konflikt vorliegt, f\u00fclle:\n- fehler_flags: Liste der Fehlercodes\n- fehler_details: kurze Begr\u00fcndungen mit Bezug auf Rohdaten/Config\n- unsicherheit_score: 0.0 (keine Unsicherheit) bis 1.0 (maximal)\n- hinweise: Empfehlung was ein Mensch pr\u00fcfen sollte\n\n4) WENN ALLES KLAR IST\nWenn keine Unsicherheit besteht:\n- lasse \"_debug_llm3\" weg oder setze es auf ein leeres Objekt {}.\nHalluziniere KEINE Debug-Fehler.",
              "type": "string"
            },
            {
              "id": "e330cf53-5f13-4a48-b607-72052276d414",
              "name": "row_scan_context",
              "value": "={{ $json.Input.row_scan_context }}",
              "type": "string"
            },
            {
              "id": "d992aa74-1812-49fd-8c1a-c1674cf772bc",
              "name": "page_scan",
              "value": "={{ $json.Input.page_scan }}",
              "type": "string"
            },
            {
              "id": "bc119282-b1bd-4d2a-957a-7f0005c86ce9",
              "name": "page_scan_minus_1",
              "value": "={{ $json.Input.page_scan_minus_1 }}",
              "type": "string"
            },
            {
              "id": "9fd00e7f-86a7-4171-86df-fa149bce1adc",
              "name": "page_scan_minus_2",
              "value": "={{ $json.Input.page_scan_minus_2 }}",
              "type": "string"
            },
            {
              "id": "9ec316ce-f07e-463f-bfa8-60d745989137",
              "name": "page_scan_minus_3",
              "value": "={{ $json.Input.page_scan_minus_3 }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2976,
        1456
      ],
      "id": "cb77f8ac-0714-4b5e-9d0d-184e40935229",
      "name": "SET_LLM3_Prompt"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "======================================================================\nUSER-INPUT: EXTRAKTION & BLOCK-LOGIK (TRANSKRIBUS-FIRST)\n=====================================================================\nACHTUNG ROBOTER: Dies ist eine isolierte Einzelzeile ohne Kontext.\n1. Ignoriere, was in der Zeile davor stand.\n2. Wenn eine Zelle leer ist, schreibe zwingend: [LEER]\n3. Erfinde nichts. Dein Weltwissen ist verboten.\n\n### 1. DIE DATEN\n\nPRIM\u00c4RQUELLE \u2013 TRANSKRIBUS OCR (VERTRAUEN!):\n{{ JSON.stringify($json['LLM 2'].cleaned_spalten) }}\n\nKlassifizierung: {{ $json['LLM 1'].zeilen_klassifikation.typ }}\n\nVISION (zwei Scans als Anhang) NUR F\u00dcR:\n- Zellen die Transkribus LEER gelassen hat \u2192 visuell pr\u00fcfen\n- Symbole: \u00f7, ./., DITO-Zeichen, Entf\u00e4llt-Markierungen\n- Tintenfarbe: rot, blau, Bleistift\n- Streichungen: physische Linie DURCH den Textk\u00f6rper\n- Rote Nachtr\u00e4ge die Transkribus nicht erfasst hat\n\nNIEMALS: Transkribus-Text durch visuellen Eindruck oder\nWeltwissen ersetzen. \"Adviun\" bleibt \"Adviun\".\n\"Lamberger\" bleibt \"Lamberger\".\n\nDie Scans:\n- Detail-Scan (enger Ausschnitt): f\u00fcr Symbole, Farben, Streichungen\n- Kontext-Scan (mehr Padding): f\u00fcr DITO-Erkennung, kleine Zeichen\n\n---\n\n### 2. DEINE REGELN (AUS DER CONFIG)\n{{ $json.Input.config_sektion_zwei }}\n\n- Tintenfarben: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.tintenfarben || {}) }}\n- Entf\u00e4llt-Symbole: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.entfaellt_logik?.symbole || []) }}\n- DITO-Symbole: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.wiederholungs_logik?.symbole || []) }}\n\nA) DITO-LOGIK (Wiederholungen)\n- Marker: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.wiederholungs_logik?.marker || \"[DITO]\" }}\n- Symbole: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.wiederholungs_logik?.symbole || []) }}\n\nB) BLOCK-LOGIK (Mustererkennung)\n- Aktiv: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.aktiv ? \"JA\" : \"NEIN\" }}\n- Quell-Spalte: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.quelle_spalte || \"spalte_2\" }}\n- Start-Muster: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.startmuster_hinweise || []) }}\n- Fortsetzung: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.fortsetzungsmuster_hinweise || []) }}\n\n\u26a0\ufe0f HALLUZINATIONS-SPERRE F\u00dcR DIE QUELL-SPALTE (PFLICHT):\nDie Quell-Spalte lautet: {{ JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.kuenstlerblock_logik?.quelle_spalte || \"spalte_2\" }}\nDie Motiv-/Darstellungs-Spalte lautet: {{ \n  (() => {\n    const cfg = JSON.parse($json.Input.config_sektion_zwei);\n    const z = cfg.spalten_zuweisung_aktiv || cfg.spalten_zuweisung || {};\n    return Object.keys(z).find(k => (z[k] || \"\").includes(\"role_darstellung_motiv\")) || \"UNBEKANNT\";\n  })() \n}}\n\nREGEL: Wenn die Quell-Spalte im Scan LEER ist (keine Tinte):\n  \u2192 final-Wert der Quell-Spalte = \"[LEER]\"\n  \u2192 block_protokoll.event = \"EMPTY\"\n  \u2192 NIEMALS \"START\" setzen, auch wenn die Motiv-Spalte einen\n    Personennamen enth\u00e4lt.\n\nC) REINIGUNGS-GEBOTE (STRENG)\n- Entf\u00e4llt-Symbole (Nicht l\u00f6schen!): {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.entfaellt_logik?.symbole || []) }}\n- Rote Tinte: IST INHALT! Nicht l\u00f6schen.\n- Leerraum-Doktrin: Wenn visuell leer \u2192 [LEER]. Niemals \"\".\n\nD) SPALTEN-ROLLEN (DYNAMISCH)\nDie Motiv/Titel-Spalte (Adressen-Falle):\n{{ \n  (() => {\n    const cfg = JSON.parse($json.Input.config_sektion_zwei);\n    const z = cfg.spalten_zuweisung_aktiv || cfg.spalten_zuweisung || {};\n    return Object.keys(z).find(k => (z[k] || \"\").includes(\"role_darstellung_motiv\")) || \"KEINE_TITEL_SPALTE_DEFINIERT\";\n  })() \n}}\n\n---\n\n### 3. DEIN AUFTRAG\n\nA) EXTRAKTION\n- Transkribus-Text \u00fcbernehmen wo vorhanden\n- Vision nur f\u00fcr leere Transkribus-Zellen, Symbole, Farben, Streichungen\n- Wenn Transkribus leer + Vision leer \u2192 [LEER]\n- Wenn Transkribus leer + Vision zeigt Symbol \u2192 Symbol abschreiben\n- Wenn Transkribus Text hat \u2192 Transkribus-Text verwenden, nicht \u00fcberschreiben\n\nB) BLOCK-PROTOKOLL (MUSTER-CHECK)\n- START: Eindeutiger Personenname in Quell-Spalte\n- CONTINUE: Adresse oder Ort in Quell-Spalte\n- EMPTY: Quell-Spalte visuell leer\n- IGNORE: Administrativer Inhalt (Nummern, Datum, X)\n- DITO: Ausschlie\u00dflich DITO-Symbol in Quell-Spalte\n\nC) ADRESSEN-SCHUTZ\nAdresse in Motiv-Spalte \u2192 dort lassen, nicht verschieben.\n\n---\n\n### 4. OUTPUT SCHEMA (JSON)\n{\n  \"_meta_audit\": { \n    \"system_modus\": \"OCR-ROBOTER V26.0\",\n    \"bild_id\": \"...\"\n  },\n  \"spalten_vollstaendig_final\": {\n    \"spalte_1\": \"...\",\n    \"spalte_2\": \"...\",\n    \"spalte_3\": \"...\",\n    \"spalte_4\": \"...\",\n    \"spalte_5\": \"...\",\n    \"spalte_6\": \"...\",\n    \"spalte_7\": \"...\",\n    \"spalte_8\": \"...\",\n    \"spalte_9\": \"...\",\n    \"spalte_10\": \"...\"\n  },\n  \"block_protokoll\": { \n    \"event\": \"START | CONTINUE | EMPTY | IGNORE | DITO\",\n    \"quelle_spalte\": \"spalte_2\",\n    \"begruendung\": \"Warum dieses Event?\"\n  },\n  \"eintrag_metadata\": {},\n  \"objekt_beschreibung\": {},\n  \"verwaltung_intern\": {},\n  \"provenienz_intern\": {},\n  \"_debug_llm4\": {\n    \"fehler_flags\": [],\n    \"fehler_details\": [],\n    \"unsicherheit_score\": 0.0,\n    \"hinweise\": []\n  }\n}",
              "type": "string"
            },
            {
              "id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
              "name": "rawSystempromptText",
              "value": "======================================================================\nSYSTEM-MODUS: OCR-ROBOTER V26.1 (TRANSKRIBUS-FIRST + ANTI-OVERRIDE)\n=====================================================================\nDU BIST KEIN ASSISTENT. DU BIST EIN STRUKTUR-EXTRAKTOR.\nDu hast kein Ged\u00e4chtnis. Du kennst keine Vorzeilen.\n\nDEINE EINZIGE AUFGABE:\n\u00dcbernehme den Transkribus-OCR-Text aus den cleaned_spalten.\nNutze Vision ausschlie\u00dflich als Erg\u00e4nzung f\u00fcr L\u00fccken und Symbole.\n\n---------------------------------------------------------------------\nDIE 5 EISERNEN GESETZE\n\nGESETZ 1: TRANSKRIBUS IST PRIM\u00c4R\n- Der cleaned_spalten Text aus Schritt 2 ist deine Hauptquelle.\n- Du \u00fcberschreibst ihn NIEMALS mit deinem Weltwissen.\n- \"Adviun\" bleibt \"Adviun\". \"Lamberger\" bleibt \"Lamberger\".\n- \"B\u00e4derff\" bleibt \"B\u00e4derff\". \"B\u00e4cker\" ist verboten.\n- Deine Aufgabe ist Struktur und Erg\u00e4nzung, nicht Korrektur.\n\nGESETZ 2: ANTI-OVERRIDE (ABSOLUT \u2013 KEINE AUSNAHMEN)\nDie Transkribus-Priorit\u00e4t gilt AUSNAHMSLOS.\nEs gibt KEINEN Fall in dem du einen Transkribus-Textwert\ndurch visuelle Erkennung ersetzen darfst.\n- Auch wenn der Scan dir etwas anderes zu zeigen scheint.\n- Auch wenn du dir sicher bist.\n- Auch wenn du es im Debug dokumentierst.\n- Auch wenn du glaubst, die Korrektur w\u00e4re offensichtlich.\n\"Adviun\" bleibt \"Adviun\". \"B\u00e4derff\" bleibt \"B\u00e4derff\".\nDas Modell nach dir korrigiert OCR-Fehler \u2013 du nicht.\nJeden versuchten Override im Debug zu dokumentieren\nund trotzdem durchzuf\u00fchren ist ein REGELVERSTOSS.\n\nGESETZ 3: VISION ERG\u00c4NZT NUR\n- Nutze die Scans NUR f\u00fcr:\n  \u2192 Zellen die Transkribus LEER gelassen hat\n  \u2192 Symbole: \u00f7, ./., DITO-Zeichen, Entf\u00e4llt-Markierungen\n  \u2192 Tintenfarbe: rot, blau, Bleistift\n  \u2192 Streichungen: physische Linie DURCH den Textk\u00f6rper\n  \u2192 Rote Nachtr\u00e4ge die Transkribus nicht erfasst hat\n- NIEMALS: Einen Transkribus-Textwert durch visuellen\n  Eindruck ersetzen.\n\nGESETZ 4: TINTE IST GESETZ (AUCH ROTE!)\n- Rote Schrift, Stempel, Nachtr\u00e4ge sind DATEN. Nicht l\u00f6schen.\n- ROTE-TINTE-PR\u00c4ZISIERUNG (KRITISCH):\n  \u2192 STREICHUNG: Eine Linie verl\u00e4uft physisch DURCH den Textk\u00f6rper.\n    Nur dann: eintrag_durchgestrichen = true.\n    Text trotzdem abschreiben.\n    Block-Event NICHT automatisch EMPTY setzen.\n  \u2192 NACHTRAG: Rote Tinte steht NEBEN, UNTER oder \u00dcBER dem Text.\n    Als sichtbaren Inhalt erfassen.\n    Kein Einfluss auf Durchgestrichen-Status.\n    Kein Einfluss auf Block-Event.\n  \u2192 REGEL: Nur eine physische Linie durch den Text = Streichung.\n    Rote Tinte in der N\u00e4he = kein Beweis f\u00fcr Streichung.\n\nGESETZ 5: WEISS IST [LEER]\n- Wenn Transkribus leer UND Vision leer \u2192 \"[LEER]\"\n- Wenn Transkribus leer UND Vision zeigt Symbol \u2192 Symbol abschreiben\n- Niemals leeren String \"\" oder null schreiben.\n\nGESETZ 6: SPALTEN-ISOLATION\n- Jede Spalte enth\u00e4lt NUR was physisch dort steht.\n- Quell-Spalte leer \u2192 Block-Event = \"EMPTY\". Kein START.\n- Motiv-Spalte: Personennamen dort sind Bildmotive, keine Entit\u00e4ten.\n\n---------------------------------------------------------------------\nDITO-ERKENNUNG (KRITISCH)\nSchreibe [DITO] wenn du eines dieser Zeichen siehst:\n- \" \u201e \u3003 (klassische Anf\u00fchrungszeichen / Ditto-Marks)\n- Einen kleinen Strich, Haken oder H\u00e4kchen das kein Buchstabe ist\n- Ein isoliertes Zeichen das wie eine 4, ein v oder \u2193 aussieht\n- JEDES Zeichen in einer Zelle das kein vollst\u00e4ndiges Wort ist\n  und nicht als Zahl oder bekanntes Symbol erkennbar ist\nWICHTIG: Wenn Transkribus die Zelle leer l\u00e4sst aber du visuell\nein kleines Zeichen siehst \u2192 pr\u00fcfe ob es ein DITO-Zeichen ist.\n\n---------------------------------------------------------------------\nFORMATIERUNG\n- DITO-Zeichen \u2192 \"[DITO]\"\n- Durchgestrichener Text: abschreiben, Status notieren.\n- Nur JSON. Keine Erkl\u00e4rungen.\n\n---------------------------------------------------------------------\nDEBUGGING-PFLICHT\nFehler-Flags wenn n\u00f6tig:\n- OCR_UNKLAR, BLOCKEVENT_UNSICHER, DITO_KONFLIKT,\n  ROTE_TINTE_KONFLIKT\nWenn alles klar: _debug_llm4 weglassen oder {}.\nVERBOTEN: Einen Override dokumentieren und trotzdem ausf\u00fchren.",
              "type": "string"
            },
            {
              "id": "e330cf53-5f13-4a48-b607-72052276d414",
              "name": "row_scan_context",
              "value": "={{ $json.Input.row_scan_context }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2960,
        1888
      ],
      "id": "b5250efe-4691-4d31-aa19-2327ad1dcec8",
      "name": "SET_LLM4_Prompt"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Entity_Split | v1.0 \u2550\u2550\u2550\n// Zweck: Fan-out: GLiNER2-Entit\u00e4ten \u2192 ein Item pro Entit\u00e4t\n// Schreiber-Extraktion liest aus der SeaTable-Spalte; row_scan_magenta ist die Quelle f\u00fcr die Bildreferenz\n\nconst allOutputItems = [];\n\nfunction pick(...objs) {\n  for (const o of objs) {\n    if (o !== undefined && o !== null) return o;\n  }\n  return undefined;\n}\n\n// \u2550\u2550\u2550 Schreiber-Verwechslungspaare extrahieren (aus Flow 00b Profil) \u2550\u2550\u2550\n// Nur die eine VERWECHSLUNGSPAARE-Zeile \u2014 nicht alle ~38 Beschreibungen.\n// Quelle: SeaTable-Spalte schreiber_beschreibungen (bef\u00fcllt durch Flow 00b)\nlet schreiberVerwechslungspaare = '';\ntry {\n  const inp = ($input.first().json || {}).Input || {};\n\n  // SeaTable-Spalte schreiber_beschreibungen (bef\u00fcllt durch Flow 00b)\n  let arr = [];\n  const direct = inp.schreiber_beschreibungen;\n  if (Array.isArray(direct)) {\n    arr = direct;\n  } else if (typeof direct === 'string' && direct.trim()) {\n    try {\n      const p = JSON.parse(direct);\n      arr = Array.isArray(p) ? p : [direct];\n    } catch (e) { arr = [direct]; }\n  }\n\n  // Kein Config-Fallback mehr: schreiber_beschreibungen kommt ausschlie\u00dflich aus SeaTable (Flow 00b)\n\n  const vp = arr.find(s => typeof s === 'string' && s.startsWith('VERWECHSLUNGSPAARE'));\n  schreiberVerwechslungspaare = vp || '';\n} catch (e) { /* Profil fehlt oder unparsebar \u2192 leer, Prompt-Fallback greift */ }\n\nconst gndTypeMapping = {\n  'person':      'Person',\n  'ort':         'PlaceOrGeographicName',\n  'institution': 'CorporateBody',\n  'sonstige':    'SubjectHeading'\n};\n\nfor (const item of items) {\n  try {\n    const inputJson = item.json || {};\n\n    // Magenta-Bild (mit Spaltenhilfslinien) \u2014 prim\u00e4re Bildreferenz\n    const rowScanMagenta = pick(\n      inputJson.Input?.row_scan_magenta,\n      inputJson.row_scan_magenta\n    ) || '';\n\n    const validierung = pick(inputJson.validierung, inputJson.Input?.validierung) || null;\n    const yearSpan    = pick(inputJson.Input?.year_span, inputJson.year_span) || null;\n\n    // Row-ID aus _id oder aus URL ableiten\n    let rowId = inputJson._id || null;\n    if (!rowId && rowScanMagenta && typeof rowScanMagenta === 'string') {\n      try {\n        const parts = rowScanMagenta.split('/');\n        rowId = parts[parts.length - 1].replace(/_(compact_enhanced|magenta)\\.(jpg|png)$/i, '');\n      } catch(e) {}\n    }\n\n    // LLM4 Daten\n    const llm4 = pick(\n      inputJson[\"'LLM 4'\"],\n      inputJson[\"LLM 4\"],\n      inputJson[\"LLM4\"]\n    ) || {};\n\n    const cleanedSpalten = llm4.spalten_vollstaendig_final || {};\n\n    // Entit\u00e4ten aus GLiNER2\n    let entitaeten = pick(llm4.entitaeten_array, llm4.entitaeten) || [];\n    if (!Array.isArray(entitaeten)) entitaeten = [];\n\n    // FAN-OUT\n    if (entitaeten.length > 0) {\n      entitaeten.forEach((entity, index) => {\n\n        const rawType    = pick(entity.typ, entity.type) || 'person';\n        const mappedType = gndTypeMapping[rawType.toLowerCase()] || 'Person';\n        const score      = entity.score || 0.9;\n        const safeId     = `${entity.entitaet_id || 'generated_' + index}_${index}`;\n\n        allOutputItems.push({ json: {\n          entitaet_id:                  safeId,\n          entitaet_original_id:         entity.entitaet_id,\n          schreiber_verwechslungspaare: schreiberVerwechslungspaare,\n          parent_row_id:                rowId,\n          row_scan_magenta:             rowScanMagenta,\n          year_span:                    yearSpan,\n\n          entitaet_typ_mapped:          mappedType,\n          gnd_type_filter:              mappedType,\n\n          entitaet_name_original:       entity.name_original,\n          entitaet_name_norm:           entity.name_norm || entity.name_original,\n          entitaet_name_display:        entity.name_norm || entity.name_original,\n\n          // GLiNER2 liefert diese Felder nicht \u2014 Subflow 03 f\u00fcllt sie\n          entitaet_untertyp:            null,\n          entitaet_rolle:               null,\n          entitaet_beruf:               null,\n          entitaet_lebensdaten:         null,\n          entitaet_anmerkung:           null,\n\n          entitaet_quelle_spalte:       entity.quelle_spalte || null,\n          entitaet_quelle_spalte_text:  entity.original_context || '',\n          entitaet_gliner2_score:       score,\n\n          cleaned_spalten:              cleanedSpalten,\n          validierung:                  validierung\n        }});\n      });\n\n    } else {\n      allOutputItems.push({ json: {\n        entitaet_id:     'NO_ENTITY',\n        row_scan_magenta: rowScanMagenta,\n        cleaned_spalten:  cleanedSpalten\n      }});\n    }\n\n  } catch(e) {\n    allOutputItems.push({ json: {\n      error:     e.message,\n      itemIndex: items.indexOf(item)\n    }});\n  }\n}\n\nreturn allOutputItems;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2960,
        2944
      ],
      "id": "f04f048c-ccdd-4827-9b67-4b2c3bc4438e",
      "name": "FN_Entity_Split",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f68a36b8-7abf-4390-a68b-122f21d48f86",
              "name": "Input",
              "value": "={{ $('SET_Input_Row').item.json }}",
              "type": "object"
            },
            {
              "id": "ea21d02d-442b-491c-80d3-a7591669e4d8",
              "name": "LLM1_raw",
              "value": "={{ $json.choices[0].message.content }}",
              "type": "string"
            },
            {
              "id": "pass_b64_ctx",
              "name": "_context_scan_data_url",
              "value": "={{ $('FN_Load_Images').item.json._context_scan_data_url }}",
              "type": "string"
            },
            {
              "id": "pass_b64_magenta_set_llm1_raw",
              "name": "_row_scan_magenta_data_url",
              "value": "={{ $('FN_Load_Images').item.json._row_scan_magenta_data_url }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3632,
        496
      ],
      "id": "19a52fa6-8618-4b98-8dd1-8b3593f94e57",
      "name": "SET_LLM1_Raw"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_LLM1 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM1-JSON (robust) + Fehler-Tagging\n\n/**\n * CODE 1: PARSER F\u00dcR LLM 1 (Klassifikation) - Llama.cpp Edition\n * ------------------------------------------------------------\n * Nimmt den Output des LLMs, repariert kaputtes JSON (Fallback)\n * und reicht die Original-Input-Daten transparent weiter.\n */\n\nfunction universalParse(raw, label) {\n  if (!raw) throw new Error(`${label} fehlt.`);\n  if (typeof raw === \"object\") return raw;\n  if (typeof raw !== \"string\") throw new Error(`${label} muss String sein.`);\n  \n  let cleaned = raw.trim();\n\n  // 1. Markdown Code-Block Entfernung (Die h\u00e4ufigste LLM-Krankheit)\n  const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n  if (codeBlockMatch && codeBlockMatch[1]) {\n    cleaned = codeBlockMatch[1].trim();\n  } else {\n    // Versuch, alles au\u00dferhalb der \u00e4u\u00dferen geschweiften Klammern abzuschneiden\n    const firstBrace = cleaned.indexOf(\"{\");\n    const lastBrace = cleaned.lastIndexOf(\"}\");\n    if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {\n      cleaned = cleaned.substring(firstBrace, lastBrace + 1);\n    }\n  }\n\n  // 2. Erster direkter Parse-Versuch (wird dank 'response_format' oft klappen)\n  try { return JSON.parse(cleaned); } catch {}\n\n  // 3. Typografische Quotes reparieren\n  cleaned = cleaned.replace(/[\u201c\u201d]/g, '\"').replace(/[\u2018\u2019]/g, \"'\");\n  try { return JSON.parse(cleaned); } catch {}\n\n  // 4. Trailing Commas entfernen (z.B. \"key\": \"val\", } -> \"key\": \"val\" })\n  cleaned = cleaned.replace(/,\\s*([}\\]])/g, \"$1\");\n  try { return JSON.parse(cleaned); } catch {}\n  \n  throw new Error(`${label} Parsing komplett gescheitert. String war: ${cleaned.substring(0,50)}...`);\n}\n\n// Konfiguration\nconst SOURCE_FIELD = \"LLM1_raw\"; \nconst TARGET_FIELD = \"LLM 1\";\n\n// Pflichtfelder, ohne die der Flow abbrechen soll\nconst REQUIRED_FIELDS = [\n  \"zeilen_klassifikation\",\n  \"eintrag_durchgestrichen\"\n];\n\nconst out = [];\n\nfor (const item of $input.all()) {\n  try {\n    // Input-Daten mitschleifen (wichtig f\u00fcr sp\u00e4tere Nodes!)\n    const inputData = item.json.Input || item.json.input || item.json; \n    \n    // Das LLM-Ergebnis holen (Je nachdem, wie dein HTTP-Node den Output nennt)\n    // Bei OpenAI-kompatibler API steckt es oft in: item.json.choices[0].message.content\n    let rawString = item.json.choices ? item.json.choices[0].message.content : item.json[SOURCE_FIELD];\n    \n    // Parsen & Reparieren\n    const parsed = universalParse(rawString, \"LLM Output\");\n\n    // Validierung der Pflichtfelder\n    for (const f of REQUIRED_FIELDS) {\n      if (!(f in parsed)) throw new Error(`Pflichtfeld '${f}' fehlt im LLM-JSON.`);\n    }\n\n    // Polyfill: row_id aus dem Input \u00fcbernehmen\n    if (!parsed.row_id && inputData.row_id) {\n      parsed.row_id = inputData.row_id;\n    }\n\n    // Ausgabe zusammenbauen\n    out.push({\n      json: {\n        Input: inputData, // Originaldaten bleiben erhalten\n        [TARGET_FIELD]: parsed // Das saubere JSON-Objekt\n      }\n    });\n\n  } catch (error) {\n    // Fehler-Routing: L\u00e4sst den Workflow nicht abst\u00fcrzen, sondern taggt den Fehler\n    out.push({\n      json: {\n        _error: true,\n        _error_source: \"Parser LLM 1\",\n        _message: error.message,\n        Input: item.json.Input || item.json\n      }\n    });\n  }\n}\n\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3824,
        496
      ],
      "id": "bdbe9110-9813-4270-b695-3a325f4368a1",
      "name": "PARSE_LLM1"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f651fe50-2653-461c-a110-89820681fc59",
              "name": "LLM2_raw",
              "value": "={{ $json.choices[0].message.content }}",
              "type": "string"
            },
            {
              "id": "98ff869d-68b7-4766-9b61-018565ef8bc7",
              "name": "Input",
              "value": "={{ $('PARSE_LLM1').item.json.Input }}",
              "type": "object"
            },
            {
              "id": "1b50b34d-d6a9-499f-b5d1-d43c849c39f9",
              "name": "LLM 1",
              "value": "={{ $('PARSE_LLM1').item.json['LLM 1'] }}",
              "type": "object"
            },
            {
              "id": "pass_b64_ctx",
              "name": "_context_scan_data_url",
              "value": "={{ $('FN_Load_Images').item.json._context_scan_data_url }}",
              "type": "string"
            },
            {
              "id": "pass_b64_magenta_set_llm2_raw",
              "name": "_row_scan_magenta_data_url",
              "value": "={{ $('FN_Load_Images').item.json._row_scan_magenta_data_url }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3632,
        976
      ],
      "id": "9ff8099c-ec7d-4600-8ac6-589e63955a15",
      "name": "SET_LLM2_Raw"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_LLM2 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM2-JSON (robust)\n//\n// 'reparatur_hinweise' ist optional. Das optionale Debug-Feld\n// \"_debug_llm2\" wird nicht als Pflichtfeld behandelt; KNOWN_FIELDS\n// f\u00fcr universalParse deckt Debug-Keys mit ab, damit sie bei kaputten\n// JSON-Ausgaben mit repariert werden k\u00f6nnen.\n\nfunction universalParse(raw, label, knownFields = []) {\n  if (!raw) throw new Error(`${label} fehlt.`);\n  if (typeof raw === \"object\") return raw;\n  if (typeof raw !== \"string\") throw new Error(`${label} muss String sein.`);\n  let cleaned = raw.trim();\n\n  // Markdown Code-Block Entfernung (```json ... ```)\n  const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n  if (codeBlockMatch && codeBlockMatch[1]) {\n    cleaned = codeBlockMatch[1].trim();\n  } else {\n    const firstBrace = cleaned.indexOf(\"{\");\n    const lastBrace = cleaned.lastIndexOf(\"}\");\n    if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {\n      cleaned = cleaned.substring(firstBrace, lastBrace + 1);\n    } else {\n      cleaned = cleaned\n        .replace(/^```json/i, \"\")\n        .replace(/```$/, \"\")\n        .trim();\n    }\n  }\n\n  // 1. Direkter JSON.parse-Versuch\n  try {\n    return JSON.parse(cleaned);\n  } catch {}\n\n  // 2. Heuristische Reparatur f\u00fcr bekannte Felder\n  if (knownFields && knownFields.length > 0) {\n    let targeted = cleaned;\n    for (const field of knownFields) {\n      const regex = new RegExp(`(?:['\"]?)\\\\b(${field})\\\\b(?:['\"]?)\\\\s*:`, \"g\");\n      targeted = targeted.replace(regex, '\"$1\":');\n    }\n    try {\n      return JSON.parse(targeted);\n    } catch {}\n  }\n\n  // 3. Curly-Quotes normalisieren\n  cleaned = cleaned.replace(/[\u201c\u201d]/g, '\"').replace(/[\u2018\u2019]/g, \"'\");\n  try {\n    return JSON.parse(cleaned);\n  } catch {}\n\n  // 4. Keys ohne Quotes quoten + trailing Kommas entfernen\n  let r1 = cleaned\n    .replace(/([a-zA-Z0-9_]+)\\s*:/g, '\"$1\":')\n    .replace(/,(\\s*[}\\]])/g, \"$1\");\n  try {\n    return JSON.parse(r1);\n  } catch {}\n\n  // 5. Letzter Ausweg: new Function()\n  try {\n    const semiSafe = r1.replace(/[\\u0000-\\u001F]+/g, \" \");\n    const trimmed = semiSafe.trim();\n    if (!/^[\\[{]/.test(trimmed)) throw new Error(\"Kein JSON-Start erkannt.\");\n    return new Function(\"return \" + trimmed)();\n  } catch {}\n\n  throw new Error(`${label} Parsing gescheitert.`);\n}\n\nconst SOURCE_FIELD = \"LLM2_raw\";\nconst TARGET_FIELD = \"LLM 2\";\n\n// NUR \"cleaned_spalten\" ist wirklich Pflicht.\nconst REQUIRED_FIELDS = [\n  \"cleaned_spalten\"\n];\n\n// Bekannte Felder f\u00fcr heuristische Reparatur (inkl. optionalem Debug-Feld)\nconst KNOWN_FIELDS = [\n  \"cleaned_spalten\",\n  \"reparatur_hinweise\",\n  \"_meta\",\n  \"_debug_llm2\"\n];\n\nconst out = [];\nfor (const item of items) {\n  try {\n    const inputData = item.json.Input;\n    if (!inputData) throw new Error(\"Input fehlt.\");\n\n    const llm1 = item.json[\"LLM 1\"];\n    if (!llm1) throw new Error(\"LLM 1 fehlt.\");\n\n    const rawString = item.json[SOURCE_FIELD];\n\n    // universelles Parsing mit Kenntnis der wichtigsten Keys (inkl. _debug_llm2)\n    const parsed = universalParse(rawString, SOURCE_FIELD, KNOWN_FIELDS);\n\n    // --- POLYFILLS ---\n    // reparatur_hinweise ist optional \u2192 leeres Array als Default\n    if (!parsed.reparatur_hinweise) {\n      parsed.reparatur_hinweise = [];\n    }\n\n    // _meta ist optional \u2192 Default-Wrapper f\u00fcr Status\n    if (!parsed._meta) {\n      parsed._meta = { status: \"auto_generated\" };\n    }\n\n    // Validierung: wirklich kritisches Pflichtfeld\n    for (const f of REQUIRED_FIELDS) {\n      if (!(f in parsed)) throw new Error(`Feld '${f}' fehlt.`);\n    }\n\n    // parsed wird 1:1 als \"LLM 2\" \u00fcbernommen, alle Zusatzfelder\n    // (inkl. _debug_llm2) bleiben vollst\u00e4ndig erhalten.\n    out.push({\n      json: {\n        Input: inputData,\n        \"LLM 1\": llm1,\n        [TARGET_FIELD]: parsed\n      }\n    });\n  } catch (error) {\n    out.push({\n      json: {\n        _error: true,\n        _error_source: \"Parser LLM 2\",\n        _message: error.message,\n        Input: item.json.Input || null,\n        \"LLM 1\": item.json[\"LLM 1\"] || null // <-- DAS MUSS REIN\n      }\n    });\n  }\n}\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3824,
        976
      ],
      "id": "279e70f8-dd99-412c-a0d7-9ad2eaae6333",
      "name": "PARSE_LLM2"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f4936583-1e62-4fea-9001-b252677c90ba",
              "name": "LLM3_raw",
              "value": "={{ $json.choices[0].message.content }}",
              "type": "string"
            },
            {
              "id": "1ed7436b-3dc3-461f-aec1-57dc927cff62",
              "name": "Input",
              "value": "={{ $('PARSE_LLM2').item.json.Input }}",
              "type": "object"
            },
            {
              "id": "86f02154-9bb7-4980-a0b9-e9c62689dca3",
              "name": "LLM 1",
              "value": "={{ $('PARSE_LLM2').item.json['LLM 1'] }}",
              "type": "object"
            },
            {
              "id": "c9ad41f0-afeb-4832-9447-a49ed36da355",
              "name": "LLM 2",
              "value": "={{ $('PARSE_LLM2').item.json['LLM 2'] }}",
              "type": "object"
            },
            {
              "id": "pass_b64_ctx",
              "name": "_context_scan_data_url",
              "value": "={{ $('FN_Load_Images').item.json._context_scan_data_url }}",
              "type": "string"
            },
            {
              "id": "pass_b64_magenta_set_llm3_raw",
              "name": "_row_scan_magenta_data_url",
              "value": "={{ $('FN_Load_Images').item.json._row_scan_magenta_data_url }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3648,
        1456
      ],
      "id": "5a91d22d-f5f2-46dc-88bd-56683a52056b",
      "name": "SET_LLM3_Raw"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_LLM3 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM3-JSON (robust)\n//\n// 'kontext_fuer_folgezeile' wird als Polyfill gesetzt, da die echte\n// Kontext-Erstellung im Synthesizer liegt. Das optionale Debug-Feld\n// \"_debug_llm3\" wird nicht als Pflichtfeld behandelt; KNOWN_FIELDS\n// deckt Debug-Keys mit ab, damit universalParse sie robust reparieren kann.\n\nfunction universalParse(raw, label, knownFields = []) {\n  if (!raw) throw new Error(`${label} fehlt.`);\n  if (typeof raw === \"object\") return raw;\n  if (typeof raw !== \"string\") throw new Error(`${label} muss String sein.`);\n  let cleaned = raw.trim();\n\n  // Markdown Code-Block Entfernung\n  const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n  if (codeBlockMatch && codeBlockMatch[1]) {\n    cleaned = codeBlockMatch[1].trim();\n  } else {\n    const firstBrace = cleaned.indexOf(\"{\");\n    const lastBrace = cleaned.lastIndexOf(\"}\");\n    if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {\n      cleaned = cleaned.substring(firstBrace, lastBrace + 1);\n    } else {\n      cleaned = cleaned\n        .replace(/^```json/i, \"\")\n        .replace(/```$/, \"\")\n        .trim();\n    }\n  }\n\n  // 1. Direkter JSON.parse\n  try { return JSON.parse(cleaned); } catch {}\n\n  // 2. Heuristische Reparatur f\u00fcr bekannte Felder (inkl. _debug_llm3)\n  if (knownFields && knownFields.length > 0) {\n    let targeted = cleaned;\n    for (const field of knownFields) {\n      const regex = new RegExp(`(?:['\"]?)\\\\b(${field})\\\\b(?:['\"]?)\\\\s*:`, \"g\");\n      targeted = targeted.replace(regex, '\"$1\":');\n    }\n    try { return JSON.parse(targeted); } catch {}\n  }\n\n  // 3. Curly-Quotes normalisieren\n  cleaned = cleaned.replace(/[\u201c\u201d]/g, '\"').replace(/[\u2018\u2019]/g, \"'\");\n  try { return JSON.parse(cleaned); } catch {}\n\n  // 4. Aggressive Reparatur f\u00fcr Keys ohne Anf\u00fchrungszeichen\n  let r1 = cleaned\n    .replace(/([a-zA-Z0-9_]+)\\s*:/g, '\"$1\":')\n    .replace(/,(\\s*[}\\]])/g, \"$1\");\n  try { return JSON.parse(r1); } catch {}\n\n  // 5. Letzter Ausweg: new Function()\n  try {\n    const semiSafe = r1.replace(/[\\u0000-\\u001F]+/g, \" \");\n    const trimmed = semiSafe.trim();\n    if (!/^[\\[{]/.test(trimmed)) throw new Error(\"Kein JSON-Start erkannt.\");\n    return new Function(\"return \" + trimmed)();\n  } catch {}\n\n  throw new Error(`${label} Parsing gescheitert.`);\n}\n\nconst SOURCE_FIELD = \"LLM3_raw\";\nconst TARGET_FIELD = \"LLM 3\";\n\n// CRITICAL: Wir validieren nur die Kern-Daten der Inventarnummer.\nconst REQUIRED_FIELDS = [\n  \"inventarnummer\",\n  \"eintrag_durchgestrichen\"\n];\n\n// Bekannte Felder f\u00fcr heuristische Reparatur (inkl. Debug)\nconst KNOWN_FIELDS = [\n  \"inventarnummer\",\n  \"eintrag_durchgestrichen\",\n  \"kontext_fuer_folgezeile\",\n  \"_meta_image_checks\",\n  \"_debug_llm3\"\n];\n\nconst out = [];\nfor (const item of items) {\n  try {\n    const inputData = item.json.Input;\n    if (!inputData) throw new Error(\"Input fehlt.\");\n\n    const llm1 = item.json[\"LLM 1\"];\n    if (!llm1) throw new Error(\"LLM 1 fehlt.\");\n\n    const llm2 = item.json[\"LLM 2\"];\n    if (!llm2) throw new Error(\"LLM 2 fehlt.\");\n\n    const rawString = item.json[SOURCE_FIELD];\n\n    // universelles Parsing mit Hinblick auf alle relevanten Keys\n    const parsed = universalParse(rawString, SOURCE_FIELD, KNOWN_FIELDS);\n\n    // --- POLYFILLS F\u00dcR AGNOSTISCHE STABILIT\u00c4T ---\n\n    // 1. Kontext-H\u00fclle (wird im Synthesizer final bef\u00fcllt)\n    if (!parsed.kontext_fuer_folgezeile) {\n      parsed.kontext_fuer_folgezeile = { spalten_final_korrigiert: {} };\n    }\n\n    // 2. Metadaten-H\u00fclle f\u00fcr Bildpr\u00fcfungen\n    if (!parsed._meta_image_checks) {\n      parsed._meta_image_checks = [];\n    }\n\n    // --- VALIDIERUNG DER WIRKLICH KRITISCHEN FELDER ---\n    for (const f of REQUIRED_FIELDS) {\n      if (!(f in parsed)) throw new Error(`Feld '${f}' fehlt.`);\n    }\n\n    // parsed wird 1:1 als \"LLM 3\" \u00fcbernommen, inkl. optionalem _debug_llm3\n    out.push({\n      json: {\n        Input: inputData,\n        \"LLM 1\": llm1,\n        \"LLM 2\": llm2,\n        [TARGET_FIELD]: parsed\n      }\n    });\n  } catch (error) {\n    out.push({\n      json: {\n        _error: true,\n        _error_source: \"Parser LLM 3\",\n        _message: error.message,\n        Input: item.json.Input || null,\n        \"LLM 1\": item.json[\"LLM 1\"] || null,\n        \"LLM 2\": item.json[\"LLM 2\"] || null\n      }\n    });\n  }\n}\n\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3840,
        1456
      ],
      "id": "a749d776-a6ec-4b15-8e48-043fb6781a83",
      "name": "PARSE_LLM3"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f4936583-1e62-4fea-9001-b252677c90ba",
              "name": "LLM4_raw",
              "value": "={{ $json.choices[0].message.content }}",
              "type": "string"
            },
            {
              "id": "35d19fe7-1262-41fe-b822-5152386df572",
              "name": "Input",
              "value": "={{ $('PARSE_LLM3').item.json.Input }}",
              "type": "object"
            },
            {
              "id": "9e286a34-51ee-4dc4-9b1c-236ed82d4240",
              "name": "LLM 1",
              "value": "={{ $('PARSE_LLM3').item.json['LLM 1'] }}",
              "type": "object"
            },
            {
              "id": "5473a188-1201-4d24-965f-7c71332cbce6",
              "name": "LLM 2",
              "value": "={{ $('PARSE_LLM3').item.json['LLM 2'] }}",
              "type": "object"
            },
            {
              "id": "01b2b3a5-cc8e-49c6-942d-7767e0dd33bc",
              "name": "LLM 3",
              "value": "={{ $('PARSE_LLM3').item.json['LLM 3'] }}",
              "type": "object"
            },
            {
              "id": "pass_b64_ctx",
              "name": "_context_scan_data_url",
              "value": "={{ $('FN_Load_Images').item.json._context_scan_data_url }}",
              "type": "string"
            },
            {
              "id": "pass_b64_magenta_set_llm4_raw",
              "name": "_row_scan_magenta_data_url",
              "value": "={{ $('FN_Load_Images').item.json._row_scan_magenta_data_url }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3632,
        1888
      ],
      "id": "726352a4-247e-4bfe-9e3b-7b3605e175e3",
      "name": "SET_LLM4_Raw"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_LLM4 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM4-JSON (robust)\n//\n// Das LLM vergisst oft das Feld '_meta_audit' \u2014 es ist daher nicht in\n// den strikten REQUIRED_FIELDS und bekommt stattdessen einen Polyfill.\n// 'block_protokoll' bekommt ebenfalls einen Fallback-Polyfill. Das\n// optionale Debug-Feld \"_debug_llm4\" wird unterst\u00fctzt.\n\nfunction universalParse(raw, label, knownFields = []) {\n  if (!raw) throw new Error(`${label} fehlt.`);\n  if (typeof raw === \"object\") return raw;\n  if (typeof raw !== \"string\") throw new Error(`${label} muss String sein.`);\n  let cleaned = raw.trim();\n\n  // Markdown Code-Block Entfernung (```json ... ```)\n  const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n  if (codeBlockMatch && codeBlockMatch[1]) {\n    cleaned = codeBlockMatch[1].trim();\n  } else {\n    const firstBrace = cleaned.indexOf(\"{\");\n    const lastBrace = cleaned.lastIndexOf(\"}\");\n    if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {\n      cleaned = cleaned.substring(firstBrace, lastBrace + 1);\n    } else {\n      cleaned = cleaned\n        .replace(/^```json/i, \"\")\n        .replace(/```$/, \"\")\n        .trim();\n    }\n  }\n\n  // 1. Direkter JSON.parse\n  try { return JSON.parse(cleaned); } catch {}\n\n  // 2. Heuristische Reparatur f\u00fcr bekannte Felder\n  if (knownFields && knownFields.length > 0) {\n    let targeted = cleaned;\n    for (const field of knownFields) {\n      const regex = new RegExp(`(?:['\"]?)\\\\b(${field})\\\\b(?:['\"]?)\\\\s*:`, \"g\");\n      targeted = targeted.replace(regex, '\"$1\":');\n    }\n    try { return JSON.parse(targeted); } catch {}\n  }\n\n  // 3. Curly-Quotes normalisieren\n  cleaned = cleaned.replace(/[\u201c\u201d]/g, '\"').replace(/[\u2018\u2019]/g, \"'\");\n  try { return JSON.parse(cleaned); } catch {}\n\n  // 4. Keys ohne Quotes \u2192 \"key\":\n  let r1 = cleaned\n    .replace(/([a-zA-Z0-9_]+)\\s*:/g, '\"$1\":')\n    .replace(/,(\\s*[}\\]])/g, \"$1\");\n  try { return JSON.parse(r1); } catch {}\n\n  // 5. Letzter Ausweg: new Function()\n  try {\n    const semiSafe = r1.replace(/[\\u0000-\\u001F]+/g, \" \");\n    const trimmed = semiSafe.trim();\n    if (!/^[\\[{]/.test(trimmed)) throw new Error(\"Kein JSON-Start erkannt.\");\n    return new Function(\"return \" + trimmed)();\n  } catch {}\n\n  throw new Error(`${label} Parsing gescheitert.`);\n}\n\nconst SOURCE_FIELD = \"LLM4_raw\";\nconst TARGET_FIELD = \"LLM 4\";\n\n// UPDATE: _meta_audit entfernt, da das LLM es gerne wegl\u00e4sst!\nconst REQUIRED_FIELDS = [\n  \"eintrag_metadata\",\n  \"spalten_vollstaendig_final\"\n];\n\n// Bekannte Felder f\u00fcr heuristische Reparatur\nconst KNOWN_FIELDS = [\n  \"_meta_audit\",\n  \"block_protokoll\",\n  \"eintrag_metadata\",\n  \"spalten_vollstaendig_final\",\n  \"verwaltung_intern\",\n  \"provenienz_intern\",\n  \"entitaeten_array\",\n  \"_debug_llm4\"\n];\n\nconst out = [];\nfor (const item of items) {\n  try {\n    const inputData = item.json.Input;\n    if (!inputData) throw new Error(\"Input fehlt.\");\n\n    const llm1 = item.json[\"LLM 1\"];\n    if (!llm1) throw new Error(\"LLM 1 fehlt.\");\n\n    const llm2 = item.json[\"LLM 2\"];\n    if (!llm2) throw new Error(\"LLM 2 fehlt.\");\n\n    const llm3 = item.json[\"LLM 3\"];\n    if (!llm3) throw new Error(\"LLM 3 fehlt.\");\n\n    const rawString = item.json[SOURCE_FIELD];\n\n    const parsed = universalParse(rawString, SOURCE_FIELD, KNOWN_FIELDS);\n\n    // --- POLYFILLS (Die Rettung f\u00fcr fehlende Felder) ---\n\n    // 1. _meta_audit (LLM vergisst das oft)\n    if (!parsed._meta_audit) {\n      parsed._meta_audit = \"FEHLT_VON_LLM\";\n    }\n\n    // 2. block_protokoll (Wichtig f\u00fcr sp\u00e4tere Nodes!)\n    if (!parsed.block_protokoll) {\n      parsed.block_protokoll = {\n        event: \"UNKNOWN\",\n        vorheriger_block_leer: false,\n        block_typ: \"MISSING\"\n      };\n    }\n\n    // 3. Provenienz (oft leer, daher optional im Output)\n    if (!parsed.provenienz_intern) {\n      parsed.provenienz_intern = {\n        rekonstruktion_text: \"\",\n        provenienzkette: []\n      };\n    }\n\n    // 4. Entit\u00e4ten (k\u00f6nnen fehlen)\n    if (!parsed.entitaeten_array) {\n      parsed.entitaeten_array = [];\n    }\n\n    // 5. Verwaltung (enth\u00e4lt oft nur die Nummer)\n    if (!parsed.verwaltung_intern) {\n      parsed.verwaltung_intern = {\n        inventarnummer_primaer: null,\n        besonderheiten_status: null,\n        preis_angabe: null\n      };\n    }\n\n    // --- VALIDIERUNG DER ABSOLUTEN PFLICHTFELDER ---\n    for (const f of REQUIRED_FIELDS) {\n      if (!(f in parsed)) throw new Error(`Feld '${f}' fehlt.`);\n    }\n\n    // Nachbearbeitung: eintrag_datum_iso ggf. neutralisieren\n    if (parsed.eintrag_metadata) {\n      parsed.eintrag_metadata.eintrag_datum_iso = null;\n    }\n\n    // parsed 1:1 als \"LLM 4\" \u00fcbernehmen (inkl. _debug_llm4, falls vorhanden)\n    out.push({\n      json: {\n        Input: inputData,\n        \"LLM 1\": llm1,\n        \"LLM 2\": llm2,\n        \"LLM 3\": llm3,\n        [TARGET_FIELD]: parsed\n      }\n    });\n  } catch (error) {\n    out.push({\n      json: {\n        _error: true,\n        _error_source: \"Parser LLM 4\",\n        _message: error.message,\n        Input: item.json.Input || null,\n        \"LLM 1\": item.json[\"LLM 1\"] || null,\n        \"LLM 2\": item.json[\"LLM 2\"] || null,\n        \"LLM 3\": item.json[\"LLM 3\"] || null\n      }\n    });\n  }\n}\n\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3808,
        1888
      ],
      "id": "01ab2f3c-7d8a-4f57-adec-c12a7582befc",
      "name": "PARSE_LLM4"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "91710b5b-de55-4a2f-ae32-440b88fc23f4",
              "name": "LLM 4.entitaeten_array",
              "value": "={{ $json.entitaeten_array }}",
              "type": "array"
            },
            {
              "id": "3fc83e01-4b61-4bdb-98af-958bf7ace229",
              "name": "LLM 4.spalten_vollstaendig_final",
              "value": "={{ $('REQ_GLiNER').item.json['LLM 4'].spalten_vollstaendig_final }}",
              "type": "object"
            },
            {
              "id": "0757a673-3eb4-43b1-b58a-e023dae5999f",
              "name": "Input.year_span",
              "value": "={{ $('REQ_GLiNER').item.json.Input.year_span }}",
              "type": "string"
            },
            {
              "id": "4e3ba3fe-99b0-4918-81e1-da5679aa97fb",
              "name": "Input.validierung",
              "value": "={{ $('REQ_GLiNER').item.json.Input.validierung }}",
              "type": "string"
            },
            {
              "id": "394619f4-7415-482e-8c7f-2bcfd16ed4e8",
              "name": "Input.row_scan_magenta",
              "value": "={{ $('REQ_GLiNER').item.json.Input.row_scan_magenta || '' }}",
              "type": "string"
            },
            {
              "id": "6739af7a-c287-459d-8cc3-3fff7456a0b7",
              "name": "Input.schreiber_beschreibungen",
              "value": "={{ $('SET_Input_Init').item.json.schreiber_beschreibungen || '' }}",
              "type": "string"
            },
            {
              "id": "09d66afa-bb49-483f-b08e-bbb252c49cb5",
              "name": "Input.config",
              "value": "={{ $('SET_Input_Init').item.json.config || '{}' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2704,
        2944
      ],
      "id": "831440eb-99ea-4cb7-80c8-bbf141b623bf",
      "name": "SET_Entity_Input"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a134748e-1417-4eaf-b603-d00b42fdcc16",
              "name": "LLM5_raw",
              "value": "={{ $json.data }}",
              "type": "array"
            },
            {
              "id": "e164c1ff-d76a-408e-b0ee-61fc6a300fd0",
              "name": "Input",
              "value": "={{ $('PARSE_LLM4').item.json.Input }}",
              "type": "object"
            },
            {
              "id": "0fad611b-c4db-4ffc-897b-367efbcc74ba",
              "name": "LLM 1",
              "value": "={{ $('PARSE_LLM4').item.json['LLM 1'] }}",
              "type": "object"
            },
            {
              "id": "8b69b7f6-f9d3-490d-ba00-0c1ec7101fc9",
              "name": "LLM 2",
              "value": "={{ $('PARSE_LLM4').item.json['LLM 2'] }}",
              "type": "object"
            },
            {
              "id": "03654eb8-07f6-4872-b049-3ec4fd6ecd75",
              "name": "LLM 3",
              "value": "={{ $('PARSE_LLM4').item.json['LLM 3'] }}",
              "type": "object"
            },
            {
              "id": "e7105c6e-ab4d-413a-945e-b1c292c34bd6",
              "name": "LLM 4",
              "value": "={{ $('PARSE_LLM4').item.json['LLM 4'] }}",
              "type": "object"
            },
            {
              "name": "validierung",
              "value": "={{ $('PARSE_LLM4').item.json.Input.validierung }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3616,
        2944
      ],
      "id": "4a83f640-c68a-407a-8c3a-c5365ca43a0a",
      "name": "SET_LLM5_Raw"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f4936583-1e62-4fea-9001-b252677c90ba",
              "name": "LLM6_raw",
              "value": "={{ $json.choices[0].message.content }}",
              "type": "string"
            },
            {
              "id": "35d19fe7-1262-41fe-b822-5152386df572",
              "name": "Input",
              "value": "={{ $('PARSE_LLM5').item.json.Input }}",
              "type": "object"
            },
            {
              "id": "b15e46d2-7391-411b-9272-1720663c482a",
              "name": "LLM 1",
              "value": "={{ $('PARSE_LLM5').item.json['LLM 1'] }}",
              "type": "object"
            },
            {
              "id": "fb4f7a28-4e96-4abc-ac51-63e9b0c89790",
              "name": "LLM 2",
              "value": "={{ $('PARSE_LLM5').item.json['LLM 2'] }}",
              "type": "object"
            },
            {
              "id": "84679c1f-f1f9-49b0-9806-899beae69f1e",
              "name": "LLM 3",
              "value": "={{ $('PARSE_LLM5').item.json['LLM 3'] }}",
              "type": "object"
            },
            {
              "id": "11f69672-2c5e-4bc3-9e31-a073c9b4e3a9",
              "name": "LLM 4",
              "value": "={{ $('PARSE_LLM5').item.json['LLM 4'] }}",
              "type": "object"
            },
            {
              "id": "d850451d-10ca-4c52-b2e3-309d3ac60443",
              "name": "LLM 5",
              "value": "={{ $('PARSE_LLM5').item.json['LLM 5'] }}",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3568,
        3280
      ],
      "id": "7b348204-0bed-4a98-8cd5-51256934ecbe",
      "name": "SET_LLM6_Raw"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_LLM5 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM5/Agent-Output (robust, Null-Enforcement)\n//\n// Optimiert f\u00fcr den Sub-Workflow mit Agent 1 & 2 (Qwen/Kimi):\n// - Extrahiert verschachtelte Daten (wikipedia_url, role) aus 'additional_data'\n// - Extrahiert das '_debug_llm5' Objekt f\u00fcr Qualit\u00e4tskontrolle\n// - Priorisiert die ID aus dem JSON-Body, mappt alles flach f\u00fcr den Synthesizer\n// - Isolator: sucht das erste '{' und letzte '}', um LLM-Gefasel\n//   (\"Hier ist das JSON:\") au\u00dferhalb der Markdown-Bl\u00f6cke wegzuschneiden\n// - Null-Enforcement: wandelt Strings wie \"null\", \"None\" oder \"\" in echte\n//   null-Werte um; erzwingt gnd_id = null, wenn confidence = \"no_match\"\n\n// Hilfsfunktion: Versucht JSON zu parsen und rasiert alles weg, was kein JSON ist.\nfunction cleanAndParseJSON(str) {\n    // Object-Passthrough \u2013 Agent_output kommt direkt als geparste Object rein\n    if (str && typeof str === 'object' && !Array.isArray(str)) return str;\n\n    if (!str || typeof str !== 'string') return null;\n    try {\n        // 1. Entferne Markdown Code-Bl\u00f6cke\n        let clean = str.replace(/```json/gi, \"\").replace(/```/g, \"\").trim();\n        \n        // 2. NEU: Isoliere das nackte JSON (Schneidet LLM-Preamble ab)\n        const firstBrace = clean.indexOf('{');\n        const lastBrace = clean.lastIndexOf('}');\n        \n        if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {\n            clean = clean.substring(firstBrace, lastBrace + 1);\n        }\n\n        return JSON.parse(clean);\n    } catch (e) {\n        return null;\n    }\n}\n\n// Hilfsfunktion: Extrahiert Daten aus dem Eingabe-Array (n8n Struktur-Agnostik)\nfunction getRawArray(json) {\n    if (Array.isArray(json.LLM5_raw)) return json.LLM5_raw;\n    if (Array.isArray(json.data)) return json.data;\n    if (Array.isArray(json.llm5_raw)) return json.llm5_raw;\n    // Fallback: Wenn direktes Mapping aus dem vorherigen Node kommt\n    if (json.Agent_output) return [json]; \n    return null;\n}\n\n// Stripped den Index-Suffix (_0, _1, _4 etc.) von der externen Wrapper-ID.\nfunction stripIndexSuffix(id) {\n    if (!id || typeof id !== 'string') return id;\n    return id.replace(/_\\d+$/, \"\");\n}\n\nconst newItems = [];\n\n// --- MAIN LOOP ---\nfor (const item of items) {\n    try {\n        const jsonCopy = { ...item.json };\n        const rawArray = getRawArray(jsonCopy);\n\n        // Cleanup alter Raw-Daten\n        delete jsonCopy.LLM5_raw;\n        delete jsonCopy.data;\n        delete jsonCopy.llm5_raw;\n\n        if (!rawArray || rawArray.length === 0) {\n            jsonCopy[\"LLM 5\"] = [{ confidence: \"no_match\", research_notes: \"Kein Input-Array vorhanden.\" }];\n            newItems.push({ json: jsonCopy });\n            continue;\n        }\n\n        const parsedLlm5Data = [];\n        // GLiNER-Entit\u00e4ten f\u00fcr sp\u00e4teres Matching\n        const glinerEntitaeten = item.json?.['LLM 4']?.entitaeten_array || \n                                  item.json?.entitaeten_array || [];\n\n        for (const entry of rawArray) {\n            const rawString = entry.Agent_output || entry.output || \"\";\n            \n            const fallbackId = stripIndexSuffix(\n                entry.entitaet_id || (entry.json ? entry.json.entitaet_id : \"unknown\")\n            );\n\n            // 1. JSON PARSING\n            const parsed = cleanAndParseJSON(rawString);\n\n            let resultEntry = {};\n\n            if (parsed) {\n                // --- ERFOLGREICHES PARSING ---\n                \n                // Extrahiere Basis-Felder\n                let gnd_id = parsed.gnd_id || parsed[\"GND-ID\"] || null;\n                let confidence = parsed.confidence || \"no_match\";\n                \n                // NEU: Null-Constraint Sanitizing\n                // Falls das LLM den String \"null\" oder \"None\" ausgibt -> echtes null\n                if (typeof gnd_id === 'string') {\n                    const lowerGnd = gnd_id.trim().toLowerCase();\n                    if (lowerGnd === 'null' || lowerGnd === 'none' || lowerGnd === '') {\n                        gnd_id = null;\n                    }\n                }\n                \n                // NEU: Hard-Enforcement - Bei no_match darf niemals eine ID existieren\n                if (confidence === \"no_match\") {\n                    gnd_id = null;\n                }\n                \n                // GLiNER-Ursprungsdaten f\u00fcr diesen Eintrag finden\n                const glinerEntry = (glinerEntitaeten || []).find(g => g.entitaet_id === (parsed.entitaet_id || fallbackId));\n\n                resultEntry = {\n                    entitaet_id: parsed.entitaet_id || fallbackId,\n                    gnd_id: gnd_id,\n                    official_name: parsed.official_name || parsed.offizieller_name || parsed[\"Offizieller Name\"] || null,\n                    confidence: confidence,\n                    research_notes: parsed.research_notes || parsed.Begr\u00fcndung || \"\",\n                    _debug: parsed._debug_llm5 || null,\n                    // Aus GLiNER durchgereicht:\n                    typ: glinerEntry?.typ || \"unbekannt\",\n                    name_original: glinerEntry?.name_original || glinerEntry?.name_norm || \"\",\n                    quelle_spalte: glinerEntry?.quelle_spalte || \"\",\n                    entitaet_name_original: glinerEntry?.name_original || \"\"\n                };\n\n                // Deep Harvest: additional_data flachklopfen\n                if (parsed.additional_data) {\n                    if (parsed.additional_data.wikipedia_url) resultEntry.wikipedia_url = parsed.additional_data.wikipedia_url;\n                    if (parsed.additional_data.role_or_profession) resultEntry.rolle = parsed.additional_data.role_or_profession;\n                    if (parsed.additional_data.start_date) resultEntry.start_date = parsed.additional_data.start_date;\n                    if (parsed.additional_data.end_date) resultEntry.end_date = parsed.additional_data.end_date;\n                }\n\n                // Veto-Logik (Legacy Support)\n                if (parsed.validation && parsed.validation.veto_ausgel\u00f6st === true) {\n                    resultEntry.confidence = \"no_match\";\n                    resultEntry.research_notes = \"[VETO] \" + resultEntry.research_notes;\n                }\n\n            } else {\n                // --- FALLBACK (TEXT PARSING) ---\n                const rawPreview = typeof rawString === 'string'\n                    ? rawString.slice(0, 100)\n                    : JSON.stringify(rawString).slice(0, 100);\n\n                resultEntry = {\n                    entitaet_id: fallbackId,\n                    confidence: \"no_match\",\n                    gnd_id: null,\n                    official_name: null,\n                    research_notes: \"PARSER ERROR: Agent 2 lieferte kein valides JSON. Raw: \" + rawPreview + \"...\",\n                    _debug: {\n                        fehler_flags: [\"PARSER_JSON_FAIL\"],\n                        unsicherheit_score: 1.0,\n                        hinweise: [\"Agent 2 Output konnte nicht als JSON geparst werden.\"]\n                    }\n                };\n\n                // Notfall-Ernte via Regex\n                if (typeof rawString === 'string') {\n                    const gndMatch = rawString.match(/GND-ID:\\s*([^\\n,\"]+)/);\n                    if (gndMatch) {\n                        let extractedGnd = gndMatch[1].trim();\n                        if (extractedGnd.toLowerCase() !== 'null' && extractedGnd.toLowerCase() !== 'none') {\n                            resultEntry.gnd_id = extractedGnd;\n                        }\n                    }\n                    \n                    const confMatch = rawString.match(/Confidence:\\s*([^\\n,\"]+)/);\n                    if (confMatch) resultEntry.confidence = confMatch[1].trim().toLowerCase();\n                }\n            }\n\n            parsedLlm5Data.push(resultEntry);\n        }\n\n        jsonCopy[\"LLM 5\"] = parsedLlm5Data;\n        newItems.push({ json: jsonCopy });\n\n    } catch (err) {\n        newItems.push({ \n            json: { \n                ...item.json, \n                _error: true,\n                _error_source: \"Parser LLM 5\",\n                error: err.message,\n                \"LLM 5\": [{ confidence: \"no_match\", research_notes: \"CRITICAL PARSER ERROR\" }] \n            } \n        });\n    }\n}\n\nreturn newItems;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4000,
        2944
      ],
      "id": "0ab1fa20-0433-4dba-b310-7dea37b27a66",
      "name": "PARSE_LLM5"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "=======================================================================\nUSER-INPUT: VISUELLER KONTEXT & AUDIT-AUFTRAG\n=====================================================================\n### 1. DIE BUCH-KONFIGURATION (DEIN SCHALTER)\n{{ $json.Input.config_sektion_zwei }}\n\n### 2. BEREITS ERFASSTE DATEN (ZUM ABGLEICH)\n- Prim\u00e4rnummer: {{ $json['LLM 3'].inventarnummer.normalisiert }}\n- Bereits identifizierte Sekund\u00e4rnummern: {{ JSON.stringify($json['LLM 3'].inventarnummer.weitere_inventarnummern) }}\n\n### 2b. SCHRIFT-VALIDATOR BEFUND\n{{ JSON.stringify($json.Input.validierung) }}\n\nSekund\u00e4rnummern k\u00f6nnen in jeder Spalte und an jedem Rand auftauchen.\nWenn der Befund f\u00fcr IRGENDEINE Spalte einen ZWEIFEL bei Ziffern markiert:\n\u2192 Erh\u00f6hte Aufmerksamkeit bei diesen Stellen im Scan\n\u2192 Die Ziffer k\u00f6nnte anders aussehen als Transkribus sie gelesen hat\n\u2192 Dokumentiere als fehler_flag: AMBIGUOUS_HANDWRITING wenn relevant\n\n### 3. VISUELLE REFERENZ (BILD-ANALYSE)\nDer zugeh\u00f6rige Scan (Detail-Scan der Zeile) ist dieser Nachricht als direkter Bildanhang unten beigef\u00fcgt.\n\n### 4. DEIN AUFTRAG\nPr\u00fcfe gem\u00e4\u00df der Config, ob ein Audit notwendig ist. Wenn ja, scanne das Bild visuell auf zus\u00e4tzliche Inventarnummern, die bisher \u00fcbersehen wurden. Achte besonders auf rote Tinte am Rand oder in der Spalte f\u00fcr Ma\u00dfe.\nGib das Ergebnis EXAKT in diesem JSON-Schema zur\u00fcck:\n\n{\n  \"_meta_auditor\": {\n    \"aktiviert_durch_config\": true,\n    \"audit_status\": \"LEER | FUND | ABBRUCH\",\n    \"reasoning\": \"Kurze Erkl\u00e4rung\"\n  },\n  \"audit_funde\": [\n    {\n      \"system\": \"Name des Systems aus der Wissensbasis\",\n      \"originalwert\": \"Was exakt im Bild steht (raw)\",\n      \"nummer\": \"Die extrahierte/bereinigte Nummer\",\n      \"position_hint\": \"links_rand|rechts_rand|unter_masse|admin_zone|unbekannt\",\n      \"ink_hint\": \"rot|bleistift|blau|violett|unbekannt\",\n      \"confidence\": \"high|medium|low\"\n    }\n  ],\n  \"stempel_fragmente\": [\"Text von erkannten Stempeln, falls vorhanden\"],\n  \"_debug_llm6\": {\n    \"fehler_flags\": [\"z.B. BLURRY_SCAN, AMBIGUOUS_HANDWRITING, DATE_VS_INV_CONFUSION, INK_COLOR_UNCLEAR\"],\n    \"unsicherheit_score\": 0.0,\n    \"hinweise\": [\"Detaillierte Beschreibung der visuellen Unsicherheit\"]\n  }\n}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2960,
        3264
      ],
      "id": "60ba7783-dce9-4265-9878-3bab5dfda027",
      "name": "SET_LLM6_Prompt"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_LLM6 | v1.0 \u2550\u2550\u2550\n// Zweck: Parst LLM6-JSON (robust)\n//\n// F\u00e4ngt das '_debug_llm6' Objekt auf und liefert Fallbacks; Polyfill\n// f\u00fcr flaches LLM-Output-Schema (ohne _meta_auditor-Wrapper).\nfunction universalParse(raw, label, knownFields = []) {\n  if (!raw) throw new Error(`${label} fehlt.`);\n  if (typeof raw === \"object\") return raw;\n  if (typeof raw !== \"string\") throw new Error(`${label} muss String sein.`);\n  let cleaned = raw.trim();\n  \n  const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)\\s*```/i);\n  if (codeBlockMatch && codeBlockMatch[1]) {\n    cleaned = codeBlockMatch[1].trim();\n  } else {\n    const firstBrace = cleaned.indexOf(\"{\");\n    const lastBrace = cleaned.lastIndexOf(\"}\");\n    if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {\n      cleaned = cleaned.substring(firstBrace, lastBrace + 1);\n    } else {\n      cleaned = cleaned.replace(/^```json/i, \"\").replace(/```$/, \"\").trim();\n    }\n  }\n  try { return JSON.parse(cleaned); } catch {}\n  if (knownFields && knownFields.length > 0) {\n    let targeted = cleaned;\n    for (const field of knownFields) {\n      const regex = new RegExp(`(?:['\"]?)\\\\b(${field})\\\\b(?:['\"]?)\\\\s*:`, 'g');\n      targeted = targeted.replace(regex, '\"$1\":');\n    }\n    try { return JSON.parse(targeted); } catch {}\n  }\n  cleaned = cleaned.replace(/[\"\"]/g, '\"').replace(/['']/g, \"'\");\n  let r1 = cleaned.replace(/([a-zA-Z0-9_]+)\\s*:/g, '\"$1\":').replace(/,(\\s*[}\\]])/g, \"$1\");\n  try { return JSON.parse(r1); } catch {}\n  try {\n    const semiSafe = r1.replace(/[\\u0000-\\u001F]+/g, \" \");\n    const trimmed = semiSafe.trim();\n    if (!/^[\\[{]/.test(trimmed)) throw new Error(\"Kein JSON-Start erkannt.\");\n    return (new Function(\"return \" + trimmed))();\n  } catch {}\n  throw new Error(`${label} Parsing gescheitert.`);\n}\n\n// --- SCHEMA-NORMALISIERUNG ---\n// LLM 6 liefert manchmal flach, manchmal gewrappt. Wir sichern hier auch das Debug-Objekt.\nfunction normalizeLLM6(parsed) {\n  // 1. Debug-Objekt normalisieren (Fallback, falls LLM es wegl\u00e4sst)\n  const defaultDebug = {\n    fehler_flags: [],\n    unsicherheit_score: 0.0,\n    hinweise: []\n  };\n  const debugObj = parsed._debug_llm6 || defaultDebug;\n\n  // 2. Meta-Auditor normalisieren\n  let metaAuditor = parsed._meta_auditor;\n  if (!metaAuditor) {\n    metaAuditor = {\n      aktiviert_durch_config: parsed.aktiviert_durch_config ?? true,\n      audit_status: (parsed.gefundene_sekundaer_nummern?.length > 0 || parsed.stempel_fragmente?.length > 0 || parsed.audit_funde?.length > 0) ? \"FUND\" : \"LEER\",\n      reasoning: (parsed.warnungen || []).join(\" | \") || \"Kein Reasoning geliefert.\"\n    };\n  }\n\n  // 3. Audit Funde normalisieren (Kompatibilit\u00e4t mit altem \"gefundene_sekundaer_nummern\" Array)\n  let auditFunde = parsed.audit_funde;\n  if (!auditFunde && parsed.gefundene_sekundaer_nummern) {\n    auditFunde = parsed.gefundene_sekundaer_nummern.map(n => ({\n      system:        n.typ || n.system || \"unknown\",\n      nummer:        n.nummer || n.raw || \"\",\n      originalwert:  n.originalwert || n.raw || \"\",\n      position:      n.position_hint || n.position || \"\",\n      ink_hint:      n.ink_hint || \"unbekannt\",\n      confidence:    n.confidence || \"low\"\n    }));\n  } else if (!auditFunde) {\n    auditFunde = [];\n  }\n  \n  return {\n    _meta_auditor:       metaAuditor,\n    audit_funde:         auditFunde,\n    stempel_fragmente:   parsed.stempel_fragmente || [],\n    _debug_llm6:         debugObj\n  };\n}\n\nconst SOURCE_FIELD = \"LLM6_raw\";\nconst TARGET_FIELD = \"LLM 6\";\nconst REQUIRED_FIELDS = [\"_meta_auditor\"];\n\nconst out = [];\nfor (const item of items) {\n  try {\n    const i = item.json;\n    if (!i.Input) throw new Error(\"Input fehlt.\");\n    \n    const rawString = i[SOURCE_FIELD];\n    let parsed = universalParse(rawString, SOURCE_FIELD, REQUIRED_FIELDS);\n    \n    // Schema normalisieren (flach \u2192 gewrappt inkl. Debug-Polyfill)\n    parsed = normalizeLLM6(parsed);\n    \n    // Polyfills\n    if (!parsed.audit_funde) parsed.audit_funde = [];\n    \n    // Required-Check\n    for (const f of REQUIRED_FIELDS) {\n      if (!(f in parsed)) throw new Error(`Feld '${f}' fehlt.`);\n    }\n    \n    out.push({\n      json: {\n        Input:    i.Input,\n        \"LLM 1\":  i[\"LLM 1\"],\n        \"LLM 2\":  i[\"LLM 2\"],\n        \"LLM 3\":  i[\"LLM 3\"],\n        \"LLM 4\":  i[\"LLM 4\"],\n        \"LLM 5\":  i[\"LLM 5\"] || [],\n        [TARGET_FIELD]: parsed\n      }\n    });\n  } catch (error) {\n    out.push({\n      json: {\n        _error:        true,\n        _error_source: \"Parser LLM 6\",\n        _message: error.message,\n        Input: item.json.Input || null,\n        \"LLM 1\": item.json[\"LLM 1\"] || null,\n        \"LLM 2\": item.json[\"LLM 2\"] || null,\n        \"LLM 3\": item.json[\"LLM 3\"] || null,\n        \"LLM 4\": item.json[\"LLM 4\"] || null,\n        \"LLM 5\": item.json[\"LLM 5\"] || null\n      }\n    });\n  }\n}\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3760,
        3280
      ],
      "id": "e487913e-fd16-4276-9e5c-73943230bd20",
      "name": "PARSE_LLM6"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "35d19fe7-1262-41fe-b822-5152386df572",
              "name": "Input",
              "value": "={{ $('PARSE_LLM6').item.json.Input }}",
              "type": "object"
            },
            {
              "id": "9e286a34-51ee-4dc4-9b1c-236ed82d4240",
              "name": "LLM 1",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 1'] }}",
              "type": "object"
            },
            {
              "id": "5473a188-1201-4d24-965f-7c71332cbce6",
              "name": "LLM 2",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 2'] }}",
              "type": "object"
            },
            {
              "id": "01b2b3a5-cc8e-49c6-942d-7767e0dd33bc",
              "name": "LLM 3",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 3'] }}",
              "type": "object"
            },
            {
              "id": "2d2fccb0-4e74-4e7d-94e0-aa691f73fdc8",
              "name": "LLM 4",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 4'] }}",
              "type": "object"
            },
            {
              "id": "b37c6103-9d21-4e06-8c61-783808f632b9",
              "name": "LLM 5",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 5'] }}",
              "type": "array"
            },
            {
              "id": "a310b19f-b083-45e2-a567-a1d5513bf5cd",
              "name": "LLM 6",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 6'] }}",
              "type": "object"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4192,
        3376
      ],
      "id": "7b8c328c-ff19-410d-acc2-f17401a29d5d",
      "name": "SET_LLM7_Carry"
    },
    {
      "parameters": {
        "operation": "update",
        "tableName": "YOUR_TABLE_NAME",
        "rowId": "={{ $('FN_Synthesizer').item.json._id }}",
        "fieldsToSend": "autoMapInputData",
        "inputsToIgnore": "="
      },
      "type": "n8n-nodes-base.seaTable",
      "typeVersion": 2,
      "position": [
        3184,
        4080
      ],
      "id": "5b7174ae-5ffa-4201-8721-bb274d42824b",
      "name": "DB_Update_Row",
      "retryOnFail": true,
      "maxTries": 5,
      "waitBetweenTries": 5000,
      "credentials": {
        "seaTableApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "update",
        "tableName": "YOUR_TABLE_NAME",
        "rowId": "={{ $('FN_Synthesizer').item.json._id }}",
        "columnsUi": {
          "columnValues": [
            {
              "columnName": "processing_status",
              "columnValue": "ok"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.seaTable",
      "typeVersion": 2,
      "position": [
        3408,
        4080
      ],
      "id": "e2c389d4-86d3-42c1-a686-04c82ba4f706",
      "name": "DB_Update_Status"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Synthesizer | v1.0 \u2550\u2550\u2550\n// Zweck: Universal Synthesizer: f\u00fchrt LLM1-7 + Entit\u00e4ten zusammen\n//\n// Verhalten im \u00dcberblick:\n// - Block Protokoll: kontextbasierter Fallback (CONTINUE statt UNKNOWN f\u00fcr Adresszeilen)\n// - Entit\u00e4ten-Flattening inkl. Singular/Plural- und Institutions-Block-F\u00e4llen\n// - GND-IDs werden normalisiert (Whitespace/Newlines in entitaet_id entfernt)\n// - LLM7 (Provenienz & Altbestand) wird integriert\n// - Fehlt LLM4 das entitaeten_array, werden LLM5-Daten direkt verwendet;\n//   deriveTypFromId() leitet den Typ (person/ort/institution) aus der ID-Struktur ab,\n//   falls kein explizites typ-Feld vorhanden ist\n// - Sekund\u00e4rnummern aus LLM3 (Texterkennung) und LLM6 (visuell) werden zusammengef\u00fchrt\n//   und nach Nummer dedupliziert\n// - eintrag_durchgestrichen und classification_typ werden im Flattening gesetzt\n\n// --- KONSTANTEN (Datum) ---\nconst MONTH_NAMES = {\n  januar: 1, jan: 1, jaen: 1, jaenn: 1,\n  februar: 2, feb: 2, febr: 2,\n  marz: 3, maerz: 3, maer: 3, mrz: 3, mr: 3, mar: 3,\n  april: 4, apr: 4,\n  mai: 5,\n  juni: 6, jun: 6,\n  juli: 7, jul: 7,\n  august: 8, aug: 8,\n  september: 9, sep: 9, sept: 9,\n  oktober: 10, okt: 10, okl: 10,\n  november: 11, nov: 11,\n  dezember: 12, dez: 12\n};\nconst ROMAN_MONTHS = { I:1, II:2, III:3, IV:4, V:5, VI:6, VII:7, VIII:8, IX:9, X:10, XI:11, XII:12 };\nconst ILLEGIBLE_MARKERS = ['unleserlich', 'fehlt', 'gestrichen', 'ung\u00fcltig', 'xx'];\n\n// ==========================================================\n// 0. WASCHGANG: ROBOTER-SPRACHE ENTFERNEN\n// ==========================================================\nlet rows = items.map(item => item.json);\n\nfunction cleanLeerRecursively(obj) {\n  if (!obj || typeof obj !== 'object') return obj;\n  for (const key in obj) {\n    if (typeof obj[key] === 'string' && obj[key].trim() === \"[LEER]\") {\n      obj[key] = \"\";\n    } else if (typeof obj[key] === 'object') {\n      cleanLeerRecursively(obj[key]);\n    }\n  }\n  return obj;\n}\n\nrows = rows.map(row => cleanLeerRecursively(row));\n\n// ==========================================================\n// 1. HELPER FUNCTIONS\n// ==========================================================\n\nfunction unpackComplexValue(val) {\n  if (val === null || val === undefined) return \"\";\n  if (typeof val === \"string\") return val;\n  if (typeof val === \"number\") return String(val);\n  if (typeof val === \"object\") {\n    const candidates = [val.text, val.original, val.wert, val.content, val.value, val.material, val.technik, val.Text, val.Wert, val.final];\n    const found = candidates.find(c => c !== null && c !== undefined && (typeof c === \"string\" || typeof c === \"number\"));\n    if (found !== undefined) return String(found);\n    try {\n      const values = Object.values(val).filter(v => v !== null && v !== undefined && typeof v !== \"object\");\n      if (values.length > 0) return values.map(String).join(\" \");\n      return JSON.stringify(val).replace(/[\"{}]/g, \"\");\n    } catch (e) {\n      return \"Komplexes Objekt\";\n    }\n  }\n  return String(val);\n}\n\nfunction getSafeConfig(rawInput) {\n  if (!rawInput) return {};\n  if (typeof rawInput === \"object\") return rawInput;\n  const text = String(rawInput);\n  const firstBrace = text.indexOf(\"{\");\n  const lastBrace = text.lastIndexOf(\"}\");\n  let jsonCandidate = text;\n  if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {\n    jsonCandidate = text.substring(firstBrace, lastBrace + 1);\n  }\n  const sanitized = jsonCandidate.replace(/[\\u200B-\\u200D\\uFEFF]/g, \"\");\n  try {\n    return JSON.parse(sanitized);\n  } catch (e) {\n    try {\n      return JSON.parse(sanitized.replace(/\\\\/g, \"\\\\\\\\\"));\n    } catch (e2) {\n      return {};\n    }\n  }\n}\n\nfunction normalizeText(text) {\n  if (!text) return \"\";\n  return String(text)\n    .toLowerCase()\n    .replace(/\u00e4/g, \"a\").replace(/\u00f6/g, \"o\").replace(/\u00fc/g, \"u\").replace(/\u00df/g, \"ss\")\n    .replace(/\\s+/g, \" \")\n    .replace(/[.,]/g, \"\")\n    .trim();\n}\n\nfunction isIllegible(text) {\n  if (!text) return false;\n  const t = String(text).toLowerCase().trim();\n  return ILLEGIBLE_MARKERS.some(m => t.includes(m));\n}\n\nfunction sanitizeNullishString(val) {\n  if (val === null || val === undefined) return \"\";\n  if (typeof val !== \"string\") return val;\n  const cleaned = val\n    .replace(/\\u00A0/g, \" \")\n    .replace(/[\\u200B-\\u200D\\uFEFF]/g, \"\");\n  const t = cleaned.trim();\n  if (t.toLowerCase() === \"null\" || t === \"[LEER]\") return \"\";\n  return t;\n}\n\n// ==========================================================\n// 2. CLEANING LOGIC (AGNOSTISCH / CONFIG-DRIVEN)\n// ==========================================================\n\nfunction applyCleaningRules(value, colKey, cleaningRules) {\n  if (!cleaningRules || !cleaningRules[colKey]) return value;\n  let val = (value === null || value === undefined) ? \"\" : String(value);\n  const rule = cleaningRules[colKey];\n\n  if (rule.type === 'INVENTARNUMMER') {\n    if (rule.remove_brackets) val = val.replace(/[()\\[\\]]/g, \"\");\n    if (rule.fix_separators && Array.isArray(rule.fix_separators.bad_chars) && rule.fix_separators.bad_chars.length > 0) {\n      const escapedChars = rule.fix_separators.bad_chars.map(c => String(c)).filter(Boolean).map(c => c.replace(/[-\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")).join(\"\");\n      if (escapedChars) {\n        const fixRegex = new RegExp(`[${escapedChars}]`, \"g\");\n        val = val.replace(fixRegex, rule.fix_separators.good_char || \"/\");\n      }\n    }\n    if (rule.strip_trailing_markers && Array.isArray(rule.strip_trailing_markers)) {\n      const markers = rule.strip_trailing_markers.map(m => m.replace(/[-[\\]{}()*+?.,\\\\^$|#\\s]/g, '\\\\$&')).join('');\n      if (markers) {\n        const markerRegex = new RegExp(`[\\\\s${markers}]+$`, 'i');\n        val = val.replace(markerRegex, \"\");\n      }\n    }\n    if (rule.remove_spaces) val = val.replace(/\\s+/g, \"\");\n  }\n\n  if (rule.remove_leading_dito_chars) {\n    val = val.replace(/^[\"\u201e\"\u00bb\u00ab]\\s*/, \"\");\n    val = val.replace(/^\\[DITO\\]\\s*/, \"\");\n  }\n  if (rule.min_length && val.trim().length < rule.min_length) return \"\";\n  if (rule.normalize_entfaellt && /^(\\.\\/\\.|---|\u00f7|entf\u00e4llt|entf\\.?)$/i.test(val.trim())) {\n    val = rule.normalize_entfaellt;\n  }\n  if (rule.type === 'CURRENCY') {\n    val = val.replace(/\\.-$/, \",-\");\n    if (rule.decimal_separator === ',') val = val.replace(/\\.(\\d{2})$/, \",$1\");\n  }\n  if (rule.type === 'DIMENSIONS') {\n    if (rule.standardize_x) val = val.replace(/(\\d)\\s*X\\s*(\\d)/, \"$1 x $2\");\n    val = val.replace(/(\\d)x(\\d)/, \"$1 x $2\");\n  }\n  if (rule.type === 'EMPTY_CHECK') val = \"\";\n  return val.trim();\n}\n\n// ==========================================================\n// 3. SPALTEN & DITO CONFIG HELPERS\n// ==========================================================\n\nfunction extractSpaltenZuweisung(config) {\n  return config.spalten_zuweisung || config.spalten_zuweisung_aktiv || config.spaltenZuweisung || {};\n}\n\nfunction getActiveColumnKeys(spaltenZuweisung) {\n  const keys = Object.entries(spaltenZuweisung || {})\n    .filter(([k, v]) => /^spalte_\\d+$/.test(k) && v !== null && v !== undefined && String(v).trim() !== \"\")\n    .map(([k]) => k);\n  keys.sort((a, b) => parseInt(a.split(\"_\")[1], 10) - parseInt(b.split(\"_\")[1], 10));\n  return keys;\n}\n\nfunction getRightmostCol(activeCols) {\n  return activeCols.length ? activeCols[activeCols.length - 1] : \"spalte_1\";\n}\n\nfunction findColumnByRoleSubstring(spaltenZuweisung, roleNeedle) {\n  const needle = String(roleNeedle).toLowerCase();\n  for (const [col, meaning] of Object.entries(spaltenZuweisung || {})) {\n    if (!/^spalte_\\d+$/.test(col)) continue;\n    if (String(meaning || \"\").toLowerCase().includes(needle)) return col;\n  }\n  return null;\n}\n\nfunction getDittoConfig(config) {\n  const kr = config.kontext_regeln || config.kontextRegeln || {};\n  const wl = kr.wiederholungs_logik || kr.wiederholungsLogik || {};\n  const marker = (wl.marker && String(wl.marker)) || \"[DITO]\";\n  const symbole = Array.isArray(wl.symbole) ? wl.symbole.map(String) : ['\"', \"\u201e\", \"\u3003\"];\n  return { marker, symbole };\n}\n\nfunction isDittoCell(val, dittoCfg) {\n  if (val === null || val === undefined) return false;\n  const t = String(val).trim();\n  if (!t) return false;\n  if (t === dittoCfg.marker) return true;\n  return dittoCfg.symbole.some(s => t === String(s).trim());\n}\n\n// ==========================================================\n// 4. DATE PARSING LOGIC\n// ==========================================================\n\nfunction createDateConfig(config, spaltenZuweisung) {\n  const kr = config.kontext_regeln || {};\n  const df = kr.datumsfenster_iso || kr.datumsfenster || \"\";\n  const years = String(df || \"\").match(/(\\d{4}).*?(\\d{4})/);\n  let dateCol = null;\n  for (const [col, meaning] of Object.entries(spaltenZuweisung || {})) {\n    if (!/^spalte_\\d+$/.test(col)) continue;\n    const m = String(meaning || \"\").toLowerCase();\n    const looksLikeDateRole = m.includes(\"role_eintragsdatum\") || m.includes(\"role_datum\") || (m.includes(\"eintragsdatum\") && !m.includes(\"vorgabe\"));\n    const isMeasureLike = m.includes(\"role_masse\") || m.includes(\"ma\u00dfe\") || m.includes(\"masse\") || m.includes(\"gr\u00f6\u00dfe\");\n    if (looksLikeDateRole && !isMeasureLike) { dateCol = col; break; }\n  }\n  return {\n    DATE_COLUMN: dateCol,\n    PIVOT_YEAR: kr.iso_referenz_jahr || kr.datumsfenster_pivot_jahr || 1970,\n    YEAR_MIN: years ? parseInt(years[1], 10) : 1900,\n    YEAR_MAX: years ? parseInt(years[2], 10) : 2030\n  };\n}\n\nfunction isValidDate(year, month, day) {\n  return month >= 1 && month <= 12 && day >= 1 && day <= 31;\n}\n\nfunction formatISO(year, month, day = null) {\n  if (!year) return null;\n  const y = String(year).padStart(4, \"0\");\n  const m = month ? String(month).padStart(2, \"0\") : \"01\";\n  const d = day ? String(day).padStart(2, \"0\") : \"01\";\n  return `${y}-${m}-${d}`;\n}\n\nfunction normalizeYear(yearInput, DATE_CONFIG) {\n  if (yearInput >= 100) return yearInput;\n  const { YEAR_MIN, YEAR_MAX, PIVOT_YEAR } = DATE_CONFIG;\n  const c19 = 1900 + yearInput, c20 = 2000 + yearInput;\n  const in19 = c19 >= YEAR_MIN && c19 <= YEAR_MAX;\n  const in20 = c20 >= YEAR_MIN && c20 <= YEAR_MAX;\n  if (in19 && !in20) return c19;\n  if (in20 && !in19) return c20;\n  return yearInput >= (PIVOT_YEAR % 100) ? c19 : c20;\n}\n\nfunction parseDate(rawText, DATE_CONFIG) {\n  const source = `Originaltext: '${rawText}'`;\n  const warnings = [];\n  if (!rawText || String(rawText).trim() === \"\") return { iso: null, final: \"\", source: \"leer\", warnings };\n  const cleaned = String(rawText).trim();\n  if (isIllegible(cleaned)) return { iso: null, final: cleaned, source: \"unleserlich\", warnings };\n  let d = null, m = null, y = null;\n  const isoM = cleaned.match(/^(\\d{4})\\s*[.-/]\\s*(\\d{1,2})\\s*[.-/]\\s*(\\d{1,2})$/);\n  if (isoM) { y = parseInt(isoM[1],10); m = parseInt(isoM[2],10); d = parseInt(isoM[3],10); }\n  if (!y) {\n    const dm = cleaned.match(/^(\\d{1,2})\\s*[.-/]\\s*(\\d{1,2})\\s*[.-/]\\s*(\\d{2,4})$/);\n    if (dm) { d = parseInt(dm[1],10); m = parseInt(dm[2],10); y = normalizeYear(parseInt(dm[3],10), DATE_CONFIG); }\n  }\n  if (!y) {\n    const txtM = cleaned.match(/^(\\d{1,2})[.\\s]+([a-zA-Z\u00e4\u00f6\u00fc\u00c4\u00d6\u00dc]{2,})[.\\s]+(\\d{2,4})$/);\n    if (txtM) {\n      const monthClean = normalizeText(txtM[2]);\n      if (MONTH_NAMES[monthClean]) { d = parseInt(txtM[1],10); m = MONTH_NAMES[monthClean]; y = normalizeYear(parseInt(txtM[3],10), DATE_CONFIG); }\n    }\n  }\n  if (!y) {\n    const rm = cleaned.match(/^(\\d{1,2})\\s*[.-/]\\s*([IVXLCDM]+)\\s*[.-/]?\\s*(\\d{2,4})$/i);\n    if (rm && ROMAN_MONTHS[rm[2].toUpperCase()]) {\n      d = parseInt(rm[1],10); m = ROMAN_MONTHS[rm[2].toUpperCase()]; y = normalizeYear(parseInt(rm[3],10), DATE_CONFIG);\n    }\n  }\n  if (y && m && d && isValidDate(y, m, d)) return { iso: formatISO(y, m, d), final: cleaned, source, warnings };\n  if (y) return { iso: formatISO(y, 1, 1), final: cleaned, source, warnings };\n  warnings.push(\"W_DATE_PARSE_FAIL\");\n  return { iso: null, final: cleaned, source: \"fail\", warnings };\n}\n\n// ==========================================================\n// 5. MAIN PROCESSING LOOP\n// ==========================================================\n\nconst groupedItems = {};\nfor (const item of items) {\n  const json = item.json;\n  const baseData = json.Input ? json : { Input: json };\n  const id = baseData.Input._id || baseData._id || \"unknown\";\n  if (!groupedItems[id]) groupedItems[id] = { base: baseData, llm5_collected: [] };\n  if (baseData[\"LLM 5\"] && Array.isArray(baseData[\"LLM 5\"])) {\n    groupedItems[id].llm5_collected.push(...baseData[\"LLM 5\"]);\n  }\n}\n\nconst synthesizedItems = [];\n\nfor (const id in groupedItems) {\n  const group = groupedItems[id];\n  const baseData = group.base;\n  const allValidations = group.llm5_collected || [];\n\n  const finalJson = {\n    _meta: { schema_valid: true, schema_errors: [], warnings: [] },\n    kontext_fuer_folgezeile: {}\n  };\n  const debug = [];\n\n  // --- CONFIG LOADING ---\n  const rawConfig = baseData.Input.config || baseData.Input.config_sektion_zwei || baseData.Input.buch_konfiguration;\n  const config = getSafeConfig(rawConfig);\n  const spaltenZuweisung = extractSpaltenZuweisung(config);\n  const cleaningRules = config.cleaning_rules || {};\n  const activeCols = getActiveColumnKeys(spaltenZuweisung);\n  const RIGHTMOST_COL = getRightmostCol(activeCols);\n  const MAX_COL = activeCols.length ? parseInt(RIGHTMOST_COL.split(\"_\")[1], 10) : 10;\n  const dittoCfg = getDittoConfig(config);\n  const kr = config.kontext_regeln || {};\n  const kb = kr.kuenstlerblock_logik || {};\n  const artistBlockEnabled = !!kb.aktiv && !!kb.gilt_auch_bei_leerzeilen;\n  const artistColKey =\n    (kb.quelle_spalte && /^spalte_\\d+$/.test(kb.quelle_spalte) && kb.quelle_spalte) ||\n    findColumnByRoleSubstring(spaltenZuweisung, \"role_kuenstler_und_adresse\") ||\n    findColumnByRoleSubstring(spaltenZuweisung, \"kuenstler\");\n\n  function appendOverhangToRightmost(val, label) {\n    const t = sanitizeNullishString(val);\n    if (!t || (typeof t === \"string\" && t.trim() === \"\")) return;\n    const cur = finalJson.spalten_vollstaendig?.[RIGHTMOST_COL] || { rohtext: \"\", final: \"\" };\n    const sep = cur.final && String(cur.final).trim() ? \" | \" : \"\";\n    cur.final = (cur.final || \"\") + sep + `[\u00dcberhang ${label}]: ${String(t).trim()}`;\n    const sepR = cur.rohtext && String(cur.rohtext).trim() ? \" | \" : \"\";\n    cur.rohtext = (cur.rohtext || \"\") + sepR + `[\u00dcberhang ${label}]: ${String(t).trim()}`;\n    finalJson.spalten_vollstaendig[RIGHTMOST_COL] = cur;\n    debug.push(`OVERHANG->${RIGHTMOST_COL}: ${label} gemerged.`);\n  }\n\n  try {\n    const llm1 = baseData[\"LLM 1\"] || {};\n    const llm2 = baseData[\"LLM 2\"] || {};\n    const llm3 = baseData[\"LLM 3\"] || {};\n    const llm4 = baseData[\"LLM 4\"] || {};\n    const llm6 = baseData[\"LLM 6\"] || {};\n    // LLM7 Provenienz & Altbestand\n    const llm7 = baseData[\"LLM 7\"] || {};\n\n    // --- QA HARVESTER ---\n    let max_score = 0.0;\n    const all_flags = [];\n    const all_hinweise = [];\n\n    const sources = [\n      { name: \"LLM1_Klassifikation\", debug: llm1._debug_llm1 },\n      { name: \"LLM2_Reparatur\",      debug: llm2._debug_llm2 },\n      { name: \"LLM3_InvNummer\",      debug: llm3._debug_llm3 },\n      { name: \"LLM4_Transkription\",  debug: llm4._debug_llm4 },\n      { name: \"LLM6_VisualAudit\",    debug: llm6._debug_llm6 }\n    ];\n\n    for (const src of sources) {\n      if (src.debug) {\n        if (typeof src.debug.unsicherheit_score === 'number' && src.debug.unsicherheit_score > max_score) max_score = src.debug.unsicherheit_score;\n        if (Array.isArray(src.debug.fehler_flags)) src.debug.fehler_flags.forEach(flag => all_flags.push(`${src.name}:${flag}`));\n        if (src.debug.hinweise) {\n          const arr = Array.isArray(src.debug.hinweise) ? src.debug.hinweise : [src.debug.hinweise];\n          arr.forEach(h => all_hinweise.push(`[${src.name}] ${h}`));\n        }\n        if (src.debug.fehler_details) {\n          const arr = Array.isArray(src.debug.fehler_details) ? src.debug.fehler_details : [src.debug.fehler_details];\n          arr.forEach(d => all_hinweise.push(`[${src.name} Detail] ${d}`));\n        }\n      }\n    }\n\n    if (Array.isArray(allValidations)) {\n      allValidations.forEach((ent, idx) => {\n        const debugObj = ent._debug || ent._debug_llm5;\n        if (debugObj) {\n          if (typeof debugObj.unsicherheit_score === 'number' && debugObj.unsicherheit_score > max_score) max_score = debugObj.unsicherheit_score;\n          if (Array.isArray(debugObj.fehler_flags)) debugObj.fehler_flags.forEach(flag => all_flags.push(`LLM5_Entity_${idx}:${flag}`));\n          if (debugObj.hinweise) {\n            const arr = Array.isArray(debugObj.hinweise) ? debugObj.hinweise : [debugObj.hinweise];\n            arr.forEach(h => all_hinweise.push(`[LLM5_Entity_${idx}] ${h}`));\n          }\n          if (debugObj.fehler_details) {\n            const arr = Array.isArray(debugObj.fehler_details) ? debugObj.fehler_details : [debugObj.fehler_details];\n            arr.forEach(d => all_hinweise.push(`[LLM5_Entity_${idx} Detail] ${d}`));\n          }\n        }\n      });\n    }\n\n    let qa_status = \"AUTO_CLEAN\";\n    if (max_score >= 0.7) qa_status = \"CRITICAL_REVIEW\";\n    else if (max_score >= 0.3 || all_flags.length > 0) qa_status = \"REVIEW_RECOMMENDED\";\n\n    finalJson._QA_REPORT = { status: qa_status, global_unsicherheit: max_score, total_flags_count: all_flags.length, flags: all_flags, hinweise: all_hinweise };\n\n    // --- BLOCK PROTOKOLL (kontextbasierter Fallback) ---\n    let fallbackBlockProtokoll;\n    if (llm4 && typeof llm4 === \"object\" && llm4.block_protokoll && Object.keys(llm4.block_protokoll).length > 0) {\n      fallbackBlockProtokoll = llm4.block_protokoll;\n    } else {\n      let prevEvent = \"UNKNOWN\";\n      try {\n        const ctxStr = baseData.Input?.currentItem?.previous_row_corrected;\n        if (ctxStr && typeof ctxStr === \"string\" && ctxStr.trim() !== \"\" && ctxStr.trim().toLowerCase() !== \"null\") {\n          const prevCtx = JSON.parse(ctxStr);\n          prevEvent = prevCtx?.block_protokoll?.event || \"UNKNOWN\";\n        }\n      } catch(e) {}\n      const zeilenTyp = llm1?.zeilen_klassifikation?.typ || \"\";\n      const istFortsetzung = [\"START\", \"CONTINUE\"].includes(prevEvent) && [\"FOLGEZEILE\", \"ADRESSZEILE\", \"LEERZEILE_BLOCK\"].includes(zeilenTyp);\n      fallbackBlockProtokoll = {\n        event: istFortsetzung ? \"CONTINUE\" : \"UNKNOWN\",\n        quelle_spalte: \"UNBEKANNT\",\n        begruendung: `SYNTHESIZER-FALLBACK: LLM4 hat block_protokoll nicht geliefert. Prev-Event: ${prevEvent}, LLM1-Typ: ${zeilenTyp}`\n      };\n      debug.push(`BLOCK_PROTOKOLL FALLBACK: event=${fallbackBlockProtokoll.event} (prev=${prevEvent}, llm1=${zeilenTyp})`);\n    }\n    finalJson.block_protokoll = fallbackBlockProtokoll;\n    finalJson._id = baseData.Input._id;\n    finalJson._meta.row_id = baseData.Input._id;\n    finalJson._meta.classification = llm1;\n\n    // --- INVENTARNUMMER ---\n    const verwaltung = llm4.verwaltung_intern || {};\n    const inv3 = llm3.inventarnummer || {};\n    let finalInvNr = inv3.normalisiert || \"\";\n    let originalInv = inv3.original || baseData.Input.row_spalte_1 || \"\";\n    if (!finalInvNr && verwaltung.inventarnummer_primaer) finalInvNr = verwaltung.inventarnummer_primaer;\n\n    finalJson.inventarnummer = {\n      normalisiert: finalInvNr || \"\",\n      original: originalInv,\n      zusatz_informationen: inv3.zusatz_informationen || \"\",\n      weitere_inventarnummern: Array.isArray(inv3.weitere_inventarnummern) ? [...inv3.weitere_inventarnummern] : [],\n      confidence: inv3.confidence || 0,\n      begruendung: inv3.begruendung || \"\"\n    };\n\n    if (verwaltung.besonderheiten_status) {\n      const st = verwaltung.besonderheiten_status;\n      if (st !== \"false\" && st !== false && st !== null && st !== undefined && String(st).trim() !== \"\") {\n        finalJson.inventarnummer.zusatz_informationen = finalJson.inventarnummer.zusatz_informationen\n          ? (finalJson.inventarnummer.zusatz_informationen + \"; \" + st) : st;\n      }\n    }\n\n    finalJson.eintrag_durchgestrichen = String(!!llm3.eintrag_durchgestrichen || !!llm1.eintrag_durchgestrichen);\n    finalJson.alle_entitaeten_roh = llm4.entitaeten_array || [];\n    finalJson.reparatur_hinweise = llm2.reparatur_hinweise || [];\n    finalJson.extraktion_audit = llm4._meta_audit || \"\";\n    finalJson.visueller_audit_meta = llm6._meta_auditor || {};\n\n    if (llm6.audit_funde && Array.isArray(llm6.audit_funde)) {\n      finalJson.inventarnummer.weitere_inventarnummern.push(...llm6.audit_funde);\n    }\n\n    // --- PREVIOUS ROW CONTEXT ---\n    let previousRowSpalten = {};\n    try {\n      const ctxStr = baseData.Input?.currentItem?.previous_row_corrected;\n      if (ctxStr && typeof ctxStr === \"string\" && ctxStr.trim() !== \"\" && ctxStr.trim().toLowerCase() !== \"null\") {\n        const parsedCtx = JSON.parse(ctxStr);\n        previousRowSpalten = parsedCtx.spalten_final_korrigiert || parsedCtx || {};\n      }\n    } catch (e) {}\n\n    // --- SPALTEN SYNTHESE ---\n    finalJson.spalten_vollstaendig = {};\n    const spaltenSource = llm4.spalten_vollstaendig_final || {};\n    const janitorClean = llm2.cleaned_spalten || {};\n    const inputRow = baseData.Input || {};\n\n    for (const colKey of activeCols) {\n      let rawSanitized = sanitizeNullishString(inputRow[`row_${colKey}`]);\n      const janVal = janitorClean[colKey];\n      let janitorSanitized = (janVal === undefined || janVal === null) ? undefined : sanitizeNullishString(janVal);\n      let surgRaw = spaltenSource[colKey];\n      if (surgRaw && typeof surgRaw === 'object' && surgRaw.final !== undefined) surgRaw = surgRaw.final;\n      let surgeonSanitized = (surgRaw === undefined || surgRaw === null) ? undefined : sanitizeNullishString(surgRaw);\n\n      let finalVal = surgeonSanitized !== undefined ? surgeonSanitized\n                   : janitorSanitized !== undefined ? janitorSanitized\n                   : rawSanitized;\n\n      if (isDittoCell(janitorSanitized, dittoCfg) || isDittoCell(finalVal, dittoCfg)) {\n        const resolved = previousRowSpalten[colKey] || \"\";\n        if (resolved && String(resolved).trim() !== \"\") {\n          finalVal = resolved;\n          debug.push(`DITO RESOLVED ${colKey}: \"${resolved}\" aus Vorzeile.`);\n        } else {\n          finalVal = \"\";\n          debug.push(`DITO UNRESOLVED ${colKey}: keine Vorzeile vorhanden.`);\n        }\n      }\n\n      finalVal = applyCleaningRules(finalVal, colKey, cleaningRules);\n      finalJson.spalten_vollstaendig[colKey] = { rohtext: rawSanitized || \"\", final: finalVal || \"\" };\n    }\n\n    // --- INVENTARNUMMER RETTUNG ---\n    const anchorCol = findColumnByRoleSubstring(spaltenZuweisung, \"role_primaer_inventarnummer\") || getActiveColumnKeys(spaltenZuweisung)[0] || \"spalte_1\";\n    let washedAnchorVal = finalJson.spalten_vollstaendig[anchorCol]?.final || \"\";\n    if (!finalJson.inventarnummer.normalisiert && washedAnchorVal) {\n      const regexStr = cleaningRules[anchorCol]?.regex_validation;\n      if (regexStr) {\n        const regex = new RegExp(regexStr, \"i\");\n        if (regex.test(washedAnchorVal)) {\n          let perfectVal = washedAnchorVal.toUpperCase().replace(/[A-Z]$/, match => match.toLowerCase());\n          finalJson.inventarnummer.normalisiert = perfectVal;\n          debug.push(`RETTUNG: inv_nr_normalisiert aus Spalte ${anchorCol} gerettet -> ${perfectVal}`);\n        }\n      } else {\n        finalJson.inventarnummer.normalisiert = washedAnchorVal;\n        debug.push(`RETTUNG: inv_nr_normalisiert aus Spalte ${anchorCol} (ohne Regex) -> ${washedAnchorVal}`);\n      }\n    }\n\n    if (finalJson.inventarnummer?.normalisiert) {\n      let invWashed = applyCleaningRules(finalJson.inventarnummer.normalisiert, anchorCol, cleaningRules);\n      invWashed = String(invWashed).toUpperCase().replace(/[A-Z]$/, match => match.toLowerCase());\n      finalJson.inventarnummer.normalisiert = invWashed;\n      debug.push(`INV FINAL GEWASCHEN (${anchorCol}): ${invWashed}`);\n    }\n\n    // --- OVERHANG HANDLING ---\n    function harvestOverhangFrom(obj, labelPrefix) {\n      if (!obj || typeof obj !== \"object\") return;\n      for (const [k, v] of Object.entries(obj)) {\n        if (!/^spalte_\\d+$/.test(k)) continue;\n        const n = parseInt(k.split(\"_\")[1], 10);\n        if (!isNaN(n) && n > MAX_COL) appendOverhangToRightmost(v, `${labelPrefix}.${k}`);\n      }\n    }\n    function harvestOverhangFromRowInput(rowObj, labelPrefix) {\n      if (!rowObj || typeof rowObj !== \"object\") return;\n      for (const [k, v] of Object.entries(rowObj)) {\n        const m = String(k).match(/^row_(spalte_\\d+)$/);\n        if (!m) continue;\n        const n = parseInt(m[1].split(\"_\")[1], 10);\n        if (n > MAX_COL) appendOverhangToRightmost(v, `${labelPrefix}.${k}`);\n      }\n    }\n    harvestOverhangFromRowInput(inputRow, \"Input\");\n    harvestOverhangFrom(janitorClean, \"LLM2.cleaned_spalten\");\n    for (const [k, v] of Object.entries(spaltenSource)) {\n      if (!/^spalte_\\d+$/.test(k)) continue;\n      const n = parseInt(k.split(\"_\")[1], 10);\n      let valToAppend = (v && typeof v === 'object' && v.final !== undefined) ? v.final : v;\n      if (n > MAX_COL) appendOverhangToRightmost(valToAppend, `LLM4.spalten_vollstaendig_final.${k}`);\n    }\n\n    // --- K\u00dcNSTLERBLOCK-CARRY ---\n    if (artistBlockEnabled && artistColKey && finalJson.spalten_vollstaendig[artistColKey]) {\n      const cur = finalJson.spalten_vollstaendig[artistColKey].final || \"\";\n      const hasInv = !!(finalJson.inventarnummer?.normalisiert || \"\").trim();\n      const prevArtist = previousRowSpalten[artistColKey] || \"\";\n      if (hasInv && (!cur || String(cur).trim() === \"\") && prevArtist && String(prevArtist).trim() !== \"\") {\n        finalJson.spalten_vollstaendig[artistColKey].final = String(prevArtist);\n        debug.push(`BLOCK-FILL ${artistColKey}: \"${prevArtist}\" aus Vorzeile \u00fcbernommen.`);\n      }\n    }\n\n    // --- ENTITIES MERGE (ID-Normalisierung, LLM5-Fallback) ---\n    const normalizeId = id => String(id || \"\").replace(/\\s+/g, \"_\").replace(/_+/g, \"_\");\n\n    function deriveTypFromId(entitaet_id) {\n      const id = String(entitaet_id || \"\").toLowerCase();\n      if (id.includes(\"person\"))      return \"person\";\n      if (id.includes(\"ort\"))         return \"ort\";\n      if (id.includes(\"institution\")) return \"institution\";\n      if (id.includes(\"org\"))         return \"institution\";\n      return \"unbekannt\";\n    }\n\n    const exEnts = finalJson.alle_entitaeten_roh || [];\n\n    let mergedEnts;\n    if (exEnts.length > 0) {\n      mergedEnts = exEnts.map(ex => {\n        const exId = normalizeId(ex.entitaet_id);\n        const val = allValidations.find(v => {\n          if (!v || !v.entitaet_id) return false;\n          const vId = normalizeId(v.entitaet_id);\n          return vId === exId || vId.startsWith(exId + \"_\");\n        });\n        const merged = { ...ex, ...(val || {}), entitaet_id: exId, entitaet_name_original: ex.name_original || ex.entitaet_name_original };\n        if (!merged.typ) merged.typ = deriveTypFromId(exId);\n        return merged;\n      });\n    } else {\n      mergedEnts = allValidations.map(v => {\n        const vId = normalizeId(v.entitaet_id);\n        return {\n          ...v,\n          entitaet_id: vId,\n          typ: v.typ || deriveTypFromId(vId),\n          entitaet_name_original: v.official_name || v.name_original || \"\"\n        };\n      });\n    }\n\n    finalJson.entitaeten = mergedEnts;\n\n    // --- DATUM ---\n    const DATE_CONFIG = createDateConfig(config, spaltenZuweisung);\n    let dr = { iso: null, final: \"\", source: \"no_date_column\", warnings: [] };\n    if (DATE_CONFIG.DATE_COLUMN && finalJson.spalten_vollstaendig[DATE_CONFIG.DATE_COLUMN]) {\n      dr = parseDate(finalJson.spalten_vollstaendig[DATE_CONFIG.DATE_COLUMN].final || \"\", DATE_CONFIG);\n    }\n    finalJson.eintrag_metadata = { ...(llm4.eintrag_metadata || {}), ...dr };\n\n    // --- CONTEXT BUILDER ---\n    const spCtx = {};\n    for (const colKey of activeCols) {\n      spCtx[colKey] = finalJson.spalten_vollstaendig[colKey]?.final || \"\";\n    }\n    spCtx.eintragsdatum_iso = finalJson.eintrag_metadata?.iso || null;\n    spCtx.eintragsdatum_text = finalJson.eintrag_metadata?.final || \"\";\n    let bestInvNr = finalJson.inventarnummer?.normalisiert || finalJson.spalten_vollstaendig?.[anchorCol]?.final || baseData.Input[\"row_\" + anchorCol] || \"START\";\n    spCtx.letzte_inventarnummer = bestInvNr;\n    finalJson.kontext_fuer_folgezeile = { spalten_final_korrigiert: spCtx, block_protokoll: finalJson.block_protokoll || {} };\n\n    // LLM7 Provenienz & Altbestand ins finalJson schreiben\n    finalJson.llm7 = llm7;\n\n    finalJson._debug_log = debug.join(\"\\n\");\n    synthesizedItems.push({ json: finalJson });\n\n  } catch (error) {\n    synthesizedItems.push({\n      json: { _id: baseData.Input?._id, parsing_status: \"fehlerhaft\", parsing_fehler: error.message }\n    });\n  }\n}\n\n// =======================================\n// 6. FLATTENING & OUTPUT\n// =======================================\nconst finalFlatItems = [];\n\nfor (const item of synthesizedItems) {\n  const n = item.json;\n  if (n.parsing_status === \"fehlerhaft\") { finalFlatItems.push({ json: n }); continue; }\n\n  const f = {};\n  f._id = n._id;\n  f._tokens_total = n._tokens_total || null;\n\n  // --- QA ---\n  const qa = n._QA_REPORT || { status: \"UNKNOWN\", global_unsicherheit: 0.0, total_flags_count: 0, flags: [], hinweise: [] };\n  f.qa_status = qa.status;\n  f.qa_report_json = JSON.stringify(qa);\n  f._meta_warnings = (n._meta?.warnings || []).join(\"; \");\n  f._debug_log = n._debug_log || \"\";\n  f.eintragsdatum_iso = n.eintrag_metadata?.iso || null;\n  f.eintragsdatum_original = n.eintrag_metadata?.final || \"\";\n  f.kontext_fuer_folgezeile = (n.kontext_fuer_folgezeile && Object.keys(n.kontext_fuer_folgezeile).length > 0) ? JSON.stringify(n.kontext_fuer_folgezeile) : \"\";\n\n  // --- INVENTARNUMMER ---\n  const invObj = n.inventarnummer || {};\n  f.inv_nr_normalisiert = invObj.normalisiert || \"\";\n  f.inv_nr_original = invObj.original || \"\";\n  f.inv_nr_zusatz = invObj.zusatz_informationen || \"\";\n  f.inv_nr_begruendung = invObj.begruendung || \"\";\n  f.inv_nr_confidence = invObj.confidence || \"\";\n\n  const rep = n.reparatur_hinweise || [];\n  f.layout_reparatur_protokoll = rep.length > 0 ? JSON.stringify(rep) : \"\";\n  f.extraktion_audit = n.extraktion_audit || \"\";\n  f.zeilen_marker = (n._meta?.classification?.zeilen_klassifikation?.visuelle_marker || []).join(\", \");\n  f.classification_typ = n._meta?.classification?.zeilen_klassifikation?.typ || \"\";\n  f.eintrag_durchgestrichen = String(!!n.eintrag_durchgestrichen);\n  f.audit_visueller_befund = n.visueller_audit_meta?.reasoning || \"\";\n\n  // --- BLOCK PROTOKOLL ---\n  let rawKb = null;\n  if (n.block_protokoll) rawKb = n.block_protokoll.kuenstlerblock ? n.block_protokoll.kuenstlerblock : n.block_protokoll;\n  if (rawKb && Object.keys(rawKb).length > 0) {\n    const ev = rawKb.event || rawKb.Event || rawKb.EVENT || \"UNKNOWN\";\n    const reason = rawKb.begruendung || rawKb.reason || rawKb.verification || rawKb.kommentar || rawKb.erklaerung || rawKb.debug_info || rawKb.warning || \"\";\n    const srcCol = rawKb.quelle_spalte || rawKb.quell_spalte || rawKb.feld_name || rawKb.source_column || \"spalte_2\";\n    f.block_kontext = JSON.stringify({ event: ev, quelle_spalte: srcCol, reason: String(reason).trim() });\n    f.kuenstlerblock_event = ev;\n    f.kuenstlerblock_reason = String(reason).trim();\n    f.kuenstlerblock_quelle_spalte = srcCol;\n    f.kuenstlerblock_fragment_typ = rawKb.fragment_typ || rawKb.block_typ || \"\";\n    f.kuenstlerblock_confidence = rawKb.confidence || \"\";\n    f.kuenstlerblock_abschnittswechsel_visuell = rawKb.abschnittswechsel_visuell || false;\n  } else {\n    f.block_kontext = \"\"; f.kuenstlerblock_event = \"\"; f.kuenstlerblock_reason = \"\";\n    f.kuenstlerblock_quelle_spalte = \"\"; f.kuenstlerblock_fragment_typ = \"\";\n    f.kuenstlerblock_confidence = \"\"; f.kuenstlerblock_abschnittswechsel_visuell = false;\n  }\n\n  // --- SPALTEN FLATTENING ---\n  const sp = n.spalten_vollstaendig || {};\n  const existingCols = Object.keys(sp).filter(k => /^spalte_\\d+$/.test(k));\n  existingCols.sort((a,b) => parseInt(a.split(\"_\")[1],10) - parseInt(b.split(\"_\")[1],10));\n  const maxExisting = existingCols.length ? parseInt(existingCols[existingCols.length-1].split(\"_\")[1],10) : 0;\n  const FLAT_MAX_COL = Math.max(10, maxExisting);\n  for (let i = 1; i <= FLAT_MAX_COL; i++) {\n    const colKey = `spalte_${i}`;\n    f[`${colKey}_final`] = sp[colKey]?.final || \"\";\n    f[`${colKey}_rohtext`] = sp[colKey]?.rohtext || \"\";\n  }\n\n  // objekt_* entfernt \u2014 Daten stehen in spalte_*_final\n\n  // --- VERWALTUNG ---\n  const verw = n.verwaltung_intern || {};\n  const rawStempel = verw.stempel_vorhanden || verw.stempel || [];\n  f.verwaltung_stempel = Array.isArray(rawStempel) ? rawStempel.join(\", \") : String(rawStempel);\n  f.verwaltung_materialkosten = verw.materialkosten || \"\";\n\n  // =========================================================================\n  // PROVENIENZ & ALTBESTAND aus LLM7\n  // =========================================================================\n  const llm7 = n.llm7 || {};\n  f.prov_typ                 = llm7.provenienz_ereignis_typ || '';\n  f.prov_art_normalisiert    = llm7.provenienz_ereignis_typ || '';\n  f.prov_datum               = llm7.provenienz_datum_iso_von || '';\n  f.prov_preis_wert          = llm7.provenienz_preis_wert != null ? String(llm7.provenienz_preis_wert) : '';\n  f.prov_preis_waehrung      = llm7.provenienz_preis_waehrung || '';\n  f.prov_verkaufer_name      = llm7.provenienz_verkaufer_name || '';\n  f.prov_verkaufer_typ       = llm7.provenienz_verkaufer_typ || '';\n  f.prov_verkaufer_gnd_id    = llm7.provenienz_verkaufer_gnd_id || '';\n  f.prov_schenkender_name    = llm7.provenienz_schenkender_name || '';\n  f.prov_schenkender_typ     = llm7.provenienz_schenkender_typ || '';\n  f.prov_schenkender_gnd_id  = llm7.provenienz_schenkender_gnd_id || '';\n  f.prov_geber_name          = llm7.provenienz_verkaufer_name || llm7.provenienz_schenkender_name || '';\n  f.prov_transaktionsort     = llm7.provenienz_transaktionsort_name || '';\n  f.prov_transaktionsort_gnd = llm7.provenienz_transaktionsort_gnd_id || '';\n  f.prov_weitere_akteure     = llm7.provenienz_weitere_akteure_namen || '';\n  f.prov_begruendung         = llm7.provenienz_begruendung || '';\n  f.prov_rekonstruktionstext = llm7.prov_rekonstruktionstext || '';\n  f.prov_vollstaendige_kette = llm7.prov_vollstaendige_kette\n                                 ? JSON.stringify(llm7.prov_vollstaendige_kette) : '';\n  f.altbestand_ist           = String(llm7.altbestand_ist ?? false);\n  f.altbestand_kategorie     = llm7.altbestand_kategorie || 'keiner';\n  f.altbestand_indikatoren   = llm7.altbestand_indikatoren || '';\n  f.altbestand_confidence    = llm7.altbestand_confidence != null ? String(llm7.altbestand_confidence) : '';\n  // =========================================================================\n\n  // --- SEKUND\u00c4RNUMMERN (LLM3 + LLM6 zusammenf\u00fchren) ---\n  const sekFromLLM3 = Array.isArray(invObj.weitere_inventarnummern)\n    ? invObj.weitere_inventarnummern : [];\n\n  // LLM6-Funde stecken bereits in weitere_inventarnummern\n  // (Sektion 5 pusht llm6.audit_funde dorthin) \u2014 kein separater Merge n\u00f6tig.\n  const sekFromLLM6 = [];\n\n  const uniqueSek = [...sekFromLLM3, ...sekFromLLM6]\n    .filter((v,i,a) => a.findIndex(t => t.nummer === v.nummer) === i);\n\n  f.sek_inv_nr_anzahl = uniqueSek.length;\n  for (let i = 0; i < 5; i++) {\n    f[`sek_inv_nr_${i+1}_nummer`] = uniqueSek[i] ? (uniqueSek[i].nummer || uniqueSek[i].original) : null;\n    f[`sek_inv_nr_${i+1}_system`] = uniqueSek[i] ? (uniqueSek[i].system || \"Sekund\u00e4rnummer\") : null;\n  }\n\n  // --- ENTIT\u00c4TEN FLATTENING ---\n  const entSafe = n.entitaeten || [];\n\n  const personen = entSafe.filter(e => e.typ === \"person\" || e.typ === \"personen\");\n  for (let i = 0; i < 3; i++) {\n    const p = personen[i];\n    f[`person_${i+1}_name_original`]   = p ? (p.name_original || p.entitaet_name_original || \"\") : \"\";\n    f[`person_${i+1}_name_norm`]       = p ? (p.official_name || p.name_norm || \"\") : \"\";\n    f[`person_${i+1}_gnd_id`]          = p ? (p.gnd_id || \"\") : \"\";\n    f[`person_${i+1}_wiki_url`]        = p ? (p.wikipedia_url || \"\") : \"\";\n    f[`person_${i+1}_lebensdaten`]     = p ? (p.lebensdaten || p.entitaet_lebensdaten || \"\") : \"\";\n    f[`person_${i+1}_confidence`]      = p ? (p.confidence || \"\") : \"\";\n    f[`person_${i+1}_rolle`]           = p ? (p.rolle || p.entitaet_rolle || \"\") : \"\";\n    f[`person_${i+1}_recherche_notiz`] = p ? (p.research_notes || p.Begr\u00fcndung || \"\") : \"\";\n  }\n\n  const orte = entSafe.filter(e => e.typ === \"ort\" || e.typ === \"orte\" || e.typ === \"geografischer_begriff\" || e.typ === \"geografische_begriffe\");\n  for (let i = 0; i < 3; i++) {\n    const o = orte[i];\n    f[`ort_${i+1}_name_original`]    = o ? (o.name_original || o.entitaet_name_original || \"\") : \"\";\n    f[`ort_${i+1}_name_norm`]        = o ? (o.official_name || o.name_norm || \"\") : \"\";\n    f[`ort_${i+1}_gnd_id`]          = o ? (o.gnd_id || \"\") : \"\";\n    f[`ort_${i+1}_wiki_url`]        = o ? (o.wikipedia_url || \"\") : \"\";\n    f[`ort_${i+1}_lebensdaten`]     = o ? (o.lebensdaten || o.start_date || \"\") : \"\";\n    f[`ort_${i+1}_confidence`]      = o ? (o.confidence || \"\") : \"\";\n    f[`ort_${i+1}_rolle`]           = o ? (o.rolle || o.entitaet_rolle || \"\") : \"\";\n    f[`ort_${i+1}_recherche_notiz`] = o ? (o.research_notes || o.Begr\u00fcndung || \"\") : \"\";\n  }\n\n  const institutionen = entSafe.filter(e => e.typ === \"institution\" || e.typ === \"institutionen\" || e.typ === \"koerperschaft\" || e.typ === \"organisation\");\n  for (let i = 0; i < 2; i++) {\n    const inst = institutionen[i];\n    f[`institution_${i+1}_name_original`]    = inst ? (inst.name_original || inst.entitaet_name_original || \"\") : \"\";\n    f[`institution_${i+1}_name_norm`]        = inst ? (inst.official_name || inst.name_norm || \"\") : \"\";\n    f[`institution_${i+1}_gnd_id`]          = inst ? (inst.gnd_id || \"\") : \"\";\n    f[`institution_${i+1}_wiki_url`]        = inst ? (inst.wikipedia_url || \"\") : \"\";\n    f[`institution_${i+1}_lebensdaten`]     = inst ? (inst.lebensdaten || inst.start_date || \"\") : \"\";\n    f[`institution_${i+1}_confidence`]      = inst ? (inst.confidence || \"\") : \"\";\n    f[`institution_${i+1}_rolle`]           = inst ? (inst.rolle || inst.entitaet_rolle || \"\") : \"\";\n    f[`institution_${i+1}_recherche_notiz`] = inst ? (inst.research_notes || inst.Begr\u00fcndung || \"\") : \"\";\n  }\n\n  // alle_entitaeten_json: vollst\u00e4ndige Entit\u00e4ten mit quelle_spalte\n  f.alle_entitaeten_json = entSafe.length > 0 ? JSON.stringify(entSafe.map(e => ({\n    entitaet_id: e.entitaet_id || \"\",\n    typ: e.typ || \"unbekannt\",\n    name_original: e.name_original || e.entitaet_name_original || \"\",\n    name_norm: e.official_name || e.name_norm || \"\",\n    quelle_spalte: e.quelle_spalte || \"\",\n    gnd_id: e.gnd_id || null,\n    confidence: e.confidence || \"\",\n    rolle: e.rolle || \"\",\n    research_notes: e.research_notes || \"\"\n  }))) : \"\";\n\n  // FIX Bug 4/11/12: Output bereinigen vor DB-Write\n  (function(){\n    const stripM = s => (typeof s === 'string') ? s.replace(/\\[\\u00dcberhang aus spalte_\\d+\\]/gi,'').replace(/:\\s*#/g,'').replace(/\\s{2,}/g,' ').trim() : s;\n    for (const k of Object.keys(f)) { if (/^spalte_\\d+_final$/.test(k)) f[k] = stripM(f[k]); }\n    try {\n      const _e = f.alle_entitaeten_json ? JSON.parse(f.alle_entitaeten_json) : [];\n      const cnt = t => _e.filter(x => String(x.typ||'').startsWith(t)).length;\n      const tr = [];\n      if (cnt('person') > 3) tr.push('Personen'); if (cnt('ort') > 3) tr.push('Orte'); if (cnt('institution') > 2) tr.push('Institutionen');\n      if (tr.length) f._meta_warnings = (f._meta_warnings ? f._meta_warnings + ' | ' : '') + 'Slots \\u00fcberschritten: ' + tr.join(', ') + ' (siehe alle_entitaeten_json)';\n    } catch(e){}\n    delete f._debug_log;\n  })();\n  finalFlatItems.push({ json: f });\n}\n\nreturn finalFlatItems;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2960,
        4080
      ],
      "id": "5ec07a3e-d97f-4da4-9e2b-5f2262463d61",
      "name": "FN_Synthesizer"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d8f4e646-b1cf-4565-a855-923f26d73929",
              "leftValue": "={{ $json._error }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4016,
        704
      ],
      "id": "5d1d699e-02cc-4676-bb1b-c88382c54c7d",
      "name": "IF_LLM1_Error"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_LLM1_Classify | v1.0 \u2550\u2550\u2550\n// Zweck: Baut LLM1-Request (Zeilen-Klassifizierung, gemma) inkl. Bilder\n\n// LLM1: Zeilen-Klassifizierung \u2014 gemma-4-31b-qat\n// base64 kommt aus \"Input Images\" \u2014 kein Download hier.\nconst item = $input.item.json;\n// FIX Bug 1: Bilder direkt aus 'Input Images1' beziehen (Set-Nodes schleifen sie nicht durch)\nconst __imgSrc = $('FN_Load_Images').item.json;\nif (!item._row_scan_magenta_data_url) item._row_scan_magenta_data_url = __imgSrc._row_scan_magenta_data_url;\nif (!item._context_scan_data_url) item._context_scan_data_url = __imgSrc._context_scan_data_url;\n\nif (!item.rawPromptText || !item.rawSystempromptText) {\n  throw new Error('rawPromptText oder rawSystempromptText fehlt');\n}\n\nconst magentaHint = `\\nHINWEIS ZU MAGENTAFARBENEN LINIEN: Falls das Bild magentafarbene Linien enth\u00e4lt, sind diese KEINE inhaltlichen Markierungen des Inventarbuchs. Sie zeigen Spalten-Grenzen aus der automatischen Layout-Analyse (Transkribus). Nutze sie zur Orientierung bei der Spalten-Zuordnung, aber ignoriere sie bei der Inhalts-Extraktion. Jede Zelle zwischen zwei magentafarbenen vertikalen Linien entspricht genau einer Spalte gem\u00e4\u00df der Spaltendefinition.\\n`;\n\nconst userContent = [\n  { type: 'text', text: item.rawSystempromptText + '\\n\\n' + item.rawPromptText + magentaHint }\n];\n\nif (item._row_scan_magenta_data_url) {\n  userContent.push({ type: 'text', text: '\\n### VISUELLE REFERENZ 1: Detail-Scan mit Spalten-Grenzen\\nMagenta-Linien = Spalten-Grenzen, nicht Inhalt:' });\n  userContent.push({ type: 'image_url', image_url: { url: item._row_scan_magenta_data_url } });\n  // Magenta als Zeilenbild\n}\n\nif (item._context_scan_data_url) {\n  userContent.push({ type: 'text', text: '\\n### VISUELLE REFERENZ 3: Kontext-Scan (f\u00fcr DITO und K\u00fcnstlerbl\u00f6cke)' });\n  userContent.push({ type: 'image_url', image_url: { url: item._context_scan_data_url } });\n}\n\nreturn {\n  json: {\n    model: 'gemma-4-31b-qat',\n    response_format: { type: 'json_object' },\n    messages: [{ role: 'user', content: userContent }],\n    temperature: 0.1,\n    top_p: 0.9,\n    top_k: 20,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 8096,\n    reasoning_budget: 512,\n    chat_template_kwargs: { enable_thinking: false }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3184,
        496
      ],
      "id": "51c32d4a-ce77-48ee-968a-46021d51f143",
      "name": "REQ_LLM1_Classify"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_LLM3_InvNr | v1.0 \u2550\u2550\u2550\n// Zweck: Baut LLM3-Request (Inventarnummer/Forensik, qwen) inkl. Bilder\n\n// LLM3: Forensik & Prim\u00e4rnummer (Vision) \u2014 qwen3.6-27b-mtp\n// Nutzt vorberechnete base64-Bilder aus LLM1 \u2014 kein erneuter Download.\nconst item = $input.item.json;\n// FIX Bug 1: Bilder direkt aus 'Input Images1' beziehen (Set-Nodes schleifen sie nicht durch)\nconst __imgSrc = $('FN_Load_Images').item.json;\nif (!item._row_scan_magenta_data_url) item._row_scan_magenta_data_url = __imgSrc._row_scan_magenta_data_url;\nif (!item._context_scan_data_url) item._context_scan_data_url = __imgSrc._context_scan_data_url;\n\nif (!item.rawPromptText || !item.rawSystempromptText) {\n  throw new Error('Input-Felder unvollst\u00e4ndig: rawPromptText oder rawSystempromptText fehlt!');\n}\n\nconst userContent = [{ type: 'text', text: item.rawPromptText }];\n\nif item._row_scan_magenta_data_url {\n  userContent.push({ type: 'text', text: '\\n### VISUELLE REFERENZ 1: Detail-Scan mit Spalten-Grenzen (Magenta)\\nNutze dies als Ground Truth, falls der OCR-Text unklar ist:' });\n  userContent.push({ type: 'image_url', image_url: { url: item._row_scan_magenta_data_url } });\n}\nif (item._context_scan_data_url) {\n  userContent.push({ type: 'text', text: \"\\n### VISUELLE REFERENZ 2: Kontext-Scan (row_scan_context)\\nNutze dies, um zu pr\u00fcfen, ob die Nummer zu einem Block oder 'Dito' geh\u00f6rt:\" });\n  userContent.push({ type: 'image_url', image_url: { url: item._context_scan_data_url } });\n}\n\nreturn {\n  json: {\n    model: 'qwen3.6-27b-mtp',\n    response_format: { type: 'json_object' },\n    messages: [\n      { role: 'system', content: item.rawSystempromptText },\n      { role: 'user',   content: userContent }\n    ],\n    temperature: 0.1,\n    top_p: 0.8,\n    top_k: 20,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 10000,\n    reasoning_budget: 1024,\n    chat_template_kwargs: { enable_thinking: false }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3232,
        1456
      ],
      "id": "01f5bf19-290a-47ec-bd44-1bbbfba78fad",
      "name": "REQ_LLM3_InvNr"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_LLM4_Extract | v1.0 \u2550\u2550\u2550\n// Zweck: Baut LLM4-Request (Tiefen-Extraktion, qwen) inkl. Bilder\n\n// LLM4: Tiefen-Extraktion / OCR-Roboter (Vision) \u2014 qwen3.6-27b-mtp\n// V3: row_scan_magenta als prim\u00e4res Bild f\u00fcr pr\u00e4zise Spalten-Zuordnung.\n// Dense-Modell f\u00fcr strukturierte Extraktion (Transkribus-First).\nconst item = $input.item.json;\n// FIX Bug 1: Bilder direkt aus 'Input Images1' beziehen (Set-Nodes schleifen sie nicht durch)\nconst __imgSrc = $('FN_Load_Images').item.json;\nif (!item._row_scan_magenta_data_url) item._row_scan_magenta_data_url = __imgSrc._row_scan_magenta_data_url;\nif (!item._context_scan_data_url) item._context_scan_data_url = __imgSrc._context_scan_data_url;\n\nif (!item.rawPromptText || !item.rawSystempromptText) {\n  throw new Error('Input-Felder unvollst\u00e4ndig: rawPromptText oder rawSystempromptText fehlt!');\n}\n\nconst magentaHint = `\\nHINWEIS ZU MAGENTAFARBENEN LINIEN: Falls das Bild magentafarbene Linien enth\u00e4lt, sind diese KEINE inhaltlichen Markierungen. Sie zeigen Spalten-Grenzen aus der Layout-Analyse. Nutze sie zur Spalten-Zuordnung, ignoriere sie bei der Inhalts-Extraktion.\\n\\nKONSERVATIVIT\u00c4TSREGEL: Bei Unsicherheit \u00fcber einen Wert \u2014 leer lassen statt raten. Halluzinierte Werte sind schlimmer als leere Felder.\\n\\nSPALTEN-ZUORDNUNG SEKUND\u00c4RE INVENTARNUMMERN: Knorr-Nummern (z.B. 53/XI/1, 53/XI/2-11) und andere Systemnummern in roter Tinte geh\u00f6ren in spalte_8_final (Ma\u00dfe/Bemerkungen), NICHT in spalte_9_final. Spalte 9 enth\u00e4lt ausschlie\u00dflich Stempel und Admin-Vermerke (z.B. Verm\u00f6gensverzeichniskartei).\\n\\nDOPPELKLASSIFIKATION: Eine Zeile kann gleichzeitig DURCHGESTRICHEN und FORTSETZUNG sein. Beide Marker d\u00fcrfen gemeinsam gesetzt werden.\\n`;\n\nconst userContent = [{ type: 'text', text: item.rawPromptText + magentaHint }];\n\n// Prim\u00e4res Bild: Magenta wenn vorhanden\nif (item._row_scan_magenta_data_url) {\n  userContent.push({ type: 'text', text: '\\n### VISUELLE REFERENZ 1: Detail-Scan mit Spalten-Grenzen (row_scan_magenta)\\nDie magentafarbenen Linien markieren Spalten-Grenzen \u2014 f\u00fcr pr\u00e4zise Spalten-Zuordnung nutzen:' });\n  userContent.push({ type: 'image_url', image_url: { url: item._row_scan_magenta_data_url } });\n  // Magenta als Zeilenbild\n}\n\n// Kontext-Bild (ohne Magenta)\nif (item._context_scan_data_url) {\n  userContent.push({ type: 'text', text: '\\n### VISUELLE REFERENZ 3: Kontext-Scan (row_scan_context)\\nF\u00fcr DITO-Erkennung und Zeichen im Umfeld:' });\n  userContent.push({ type: 'image_url', image_url: { url: item._context_scan_data_url } });\n}\n\nreturn {\n  json: {\n    model: 'qwen3.6-27b-mtp',\n    response_format: { type: 'json_object' },\n    messages: [\n      { role: 'system', content: item.rawSystempromptText },\n      { role: 'user',   content: userContent }\n    ],\n    temperature: 0.1,\n    top_p: 0.8,\n    top_k: 20,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 10000,\n    reasoning_budget: 1024,\n    chat_template_kwargs: { enable_thinking: false }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3200,
        1888
      ],
      "id": "2bc09036-dff1-4bb5-a14c-d4a32d571f05",
      "name": "REQ_LLM4_Extract"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_LLM6_Audit | v1.0 \u2550\u2550\u2550\n// Zweck: Baut LLM6-Request (Visual Auditor, gemma) inkl. Bild\n\n// LLM6: Visual Auditor \u2014 gemma-4-12b-qat\nconst item = $input.item.json;\n\nconst scanDataUrl = $('FN_Load_Images').item.json._row_scan_magenta_data_url;\n\nif (!scanDataUrl) {\n  throw new Error(`Kein Bild verf\u00fcgbar \u2014 Zeile: ${item.ID || item._id || '?'}`);\n}\n\nreturn {\n  json: {\n    model: 'gemma-4-12b-qat',\n    messages: [{\n      role: 'user', content: [\n        { type: 'text', text: item.rawSystempromptText + '\\n\\n' + item.rawPromptText },\n        { type: 'image_url', image_url: { url: scanDataUrl } }\n      ]\n    }],\n    temperature: 0.1,\n    top_p: 0.9,\n    top_k: 20,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 4096,\n    reasoning_budget: 512,\n    chat_template_kwargs: { enable_thinking: false }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3168,
        3264
      ],
      "id": "f41a5970-66d2-4c8c-ab01-44ec619312e9",
      "name": "REQ_LLM6_Audit"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Entity_Verify_Parallel | v1.0 \u2550\u2550\u2550\n// Zweck: Ruft Entity-Verify-Subflow parallel auf (allSettled, Timeout 120s)\n\n// PARALLEL ENTITY TRIGGER \u2014 Promise.allSettled (Best Practice)\n// allSettled statt Promise.all: ein fehlschlagender Entity-Call\n// bricht nicht den ganzen Batch ab \u2014 strukturierter Fallback pro Item.\nconst items = $input.all();\nconst WEBHOOK_URL = 'http://localhost:5678/webhook/03_Entitaets_Agent';\nconst BATCH_SIZE = 2;\n\nconst ENTITY_FALLBACK = (id, err) => ({\n  entitaet_id: id || 'ERROR',\n  Agent_output: JSON.stringify({\n    confidence: 'no_match',\n    research_notes: `Fallback (allSettled): ${err}`\n  })\n});\n\nconst allResults = [];\n\nfor (let i = 0; i < items.length; i += BATCH_SIZE) {\n  const currentBatch = items.slice(i, i + BATCH_SIZE);\n\n  const batchPromises = currentBatch.map(item =>\n    this.helpers.httpRequest({\n      method: 'POST',\n      url: WEBHOOK_URL,\n      body: item.json,\n      json: true,\n      timeout: 120000\n    })\n  );\n\n  const batchResults = await Promise.allSettled(batchPromises);\n\n  for (let j = 0; j < batchResults.length; j++) {\n    const res = batchResults[j];\n    const entityId = currentBatch[j].json.entitaet_id;\n    if (res.status === 'fulfilled') {\n      allResults.push(res.value);\n    } else {\n      allResults.push(ENTITY_FALLBACK(entityId, res.reason?.message || String(res.reason)));\n    }\n  }\n}\n\nreturn { json: { data: allResults } };"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3200,
        2944
      ],
      "id": "2a1365c3-55ed-4b23-a7b1-ea0abef4b1af",
      "name": "FN_Entity_Verify_Parallel"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d8f4e646-b1cf-4565-a855-923f26d73929",
              "leftValue": "={{ $json._error }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4016,
        1200
      ],
      "id": "2381ab4a-e532-467a-ad6f-a75a0f797f9f",
      "name": "IF_LLM2_Error"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d8f4e646-b1cf-4565-a855-923f26d73929",
              "leftValue": "={{ $json._error }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4064,
        1648
      ],
      "id": "477d717d-1499-46e7-a735-cffe61064740",
      "name": "IF_LLM3_Error"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d8f4e646-b1cf-4565-a855-923f26d73929",
              "leftValue": "={{ $json._error }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        3984,
        2096
      ],
      "id": "50716d76-a639-47e8-8c84-0f2ba6de7c2a",
      "name": "IF_LLM4_Error"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d8f4e646-b1cf-4565-a855-923f26d73929",
              "leftValue": "={{ $json._error }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        3968,
        3280
      ],
      "id": "2478b593-9dda-4ac6-88cd-ee22448b7fb6",
      "name": "IF_LLM6_Error"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_GLiNER | v1.0 \u2550\u2550\u2550\n// Zweck: Baut GLiNER2-Request aus LLM2-Spalten + Config (Marker-Stripping)\n\n// =====================================================================\n// CODE-NODE: GLiNER2 Request aus LLM2 + Config aufbauen\n// =====================================================================\n\nconst item = $input.item.json;\n\n// Config parsen\nconst raw = item.Input.config_sektion_zwei;\nconst start = raw.indexOf('{');\nconst end = raw.lastIndexOf('}');\nconst config = JSON.parse(raw.substring(start, end + 1));\n\nconst zuweisung = config.spalten_zuweisung_aktiv || {};\nconst cleanedSpalten = item['LLM 2'].cleaned_spalten || {};\nconst invNr = item['LLM 3']?.inventarnummer?.normalisiert || '';\n\n// Labels pro Rolle (Dict mit Beschreibungen f\u00fcr bessere Genauigkeit)\nconst LABELS_PRO_ROLLE = {\n  'role_kuenstler_und_adresse': {\n    'artist name': 'Full name of artist in format Lastname, Firstname',\n    'city and district': 'German city with postal district, example: Berlin W 30',\n    'street': 'Street name only without house number, example: Lamberger Str.'\n  },\n  'role_freitext_admin': {\n    'institution': 'Official institution, authority or organization name',\n    'location': 'Place or city name'\n  }\n};\n\n// Verbotene Rollen\nconst VERBOTEN = [\n  'role_darstellung_motiv', 'role_technik_material',\n  'role_masse', 'role_entgelt', 'role_rahmen_foto',\n  'role_primaer_inventarnummer', 'role_materialkosten_oder_vermerk',\n  'role_meta_leer', 'role_empty_check'\n];\n\n// Spalten aufbauen\nconst spalten = [];\n\nfor (const [spalteKey, rolleText] of Object.entries(zuweisung)) {\n  if (!rolleText) continue;\n  const rolle = rolleText.split('|')[0].trim();\n  if (VERBOTEN.some(v => rolle.includes(v))) continue;\n\n  const labels = LABELS_PRO_ROLLE[rolle];\n  if (!labels) continue;\n\n  // FIX Bug 3: interne Marker entfernen, bevor Text an GLiNER geht\n  const text = (cleanedSpalten[spalteKey] || '')\n    .replace(/\\[\u00dcberhang aus spalte_\\d+\\]/gi, '')\n    .replace(/\\[LEER\\]/g, '')\n    .replace(/:\\s*#/g, '')\n    .replace(/\\s{2,}/g, ' ')\n    .trim();\n  if (!text || text === '[LEER]' || text === '[DITO]') continue;\n\n  spalten.push({ spalte: spalteKey, text, rolle, labels });\n}\n\nreturn {\n  json: {\n    ...item,\n    gliner2_request: {\n      spalten,\n      inv_nr: invNr,\n      threshold: 0.4\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2944,
        2352
      ],
      "id": "89b756a8-91df-4191-b4fc-ec99ba3e621d",
      "name": "REQ_GLiNER"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 PARSE_GLiNER | v1.0 \u2550\u2550\u2550\n// Zweck: \u00dcbernimmt entitaeten_array aus GLiNER2-Response\n\nconst item = $input.item.json;\n\n// entitaeten_array aus GLiNER2 Response holen\nconst entitaeten = item.entitaeten_array || [];\n\n// Weiterleiten an bestehenden Subflow 03\n// Format bleibt kompatibel mit dem was LLM4 bisher geliefert hat\nreturn {\n  json: {\n    ...item,\n    entitaeten_array: entitaeten,\n    _gliner2_meta: {\n      anzahl: entitaeten.length,\n      dauer_ms: item.dauer_ms || 0\n    }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3376,
        2352
      ],
      "id": "42f4aa88-8563-4255-a613-0ce9fcbd6e78",
      "name": "PARSE_GLiNER"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Load_Images | v1.0 \u2550\u2550\u2550\n// Zweck: L\u00e4dt row_scan_magenta + row_scan_context einmalig als base64\n// row_scan_magenta ist Pflichtfeld \u2014 kein Fallback n\u00f6tig\n\nconst item = $input.item.json;\n\nconst mimeFor = url => url.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';\n\nasync function downloadImage(url, label) {\n  if (!url || !url.startsWith('http')) return null;\n  try {\n    const resp = await this.helpers.httpRequest({\n      method: 'GET', url, encoding: 'arraybuffer'\n    });\n    return `data:${mimeFor(url)};base64,${Buffer.from(resp).toString('base64')}`;\n  } catch(e) {\n    console.error(`${label} Download fehlgeschlagen (${url}): ${e.message}`);\n    return null;\n  }\n}\n\nconst magentaUrl = item.row_scan_magenta;\nif (!magentaUrl || !magentaUrl.startsWith('http')) {\n  throw new Error(`row_scan_magenta fehlt oder ung\u00fcltig \u2014 Zeile: ${item.ID || item._id || '?'}`);\n}\n\nconst [magentaScanDataUrl, contextScanDataUrl] = await Promise.all([\n  downloadImage.call(this, magentaUrl, 'row_scan_magenta'),\n  item.row_scan_context ? downloadImage.call(this, item.row_scan_context, 'row_scan_context') : Promise.resolve(null)\n]);\n\nif (!magentaScanDataUrl) {\n  throw new Error(`row_scan_magenta nicht ladbar f\u00fcr Zeile ${item.ID || item._id || '?'}`);\n}\n\nreturn {\n  json: {\n    ...item,\n    _row_scan_magenta_data_url: magentaScanDataUrl,\n    _context_scan_data_url: contextScanDataUrl\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4048,
        256
      ],
      "id": "fd619138-0eac-4d1e-9d74-0a0b48c3f325",
      "name": "FN_Load_Images"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "70bb275a-d377-4464-8092-18f047d2237c",
              "name": "row_spalte_1",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_1 }}",
              "type": "string"
            },
            {
              "id": "311a465d-a0b3-40d4-8f28-54e91792d9f5",
              "name": "row_spalte_2",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_2 }}",
              "type": "string"
            },
            {
              "id": "a03a7966-a01e-48e0-8102-0cc5dc3ce1f6",
              "name": "row_spalte_3",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_3 }}",
              "type": "string"
            },
            {
              "id": "1f90cd92-41dc-407c-864c-d850a68424b7",
              "name": "row_scan_context",
              "value": "={{\n  ($('SET_Input_Init').item.json.row_scan_context || '')\n    .trim()\n    .replace(/^\\[|\\]$/g, '')\n    .replace(/^(?!https?:\\/\\/)/, 'https://')\n}}",
              "type": "string"
            },
            {
              "id": "3dd0ca7a-48d7-45cf-b25d-e5dcaec4a0c2",
              "name": "page_scan",
              "value": "={{\n  ($('SET_Input_Init').item.json.page_scan || '')\n    .trim()\n    .replace(/^\\[|\\]$/g, '')\n    .replace(/^(?!https?:\\/\\/)/, 'https://')\n}}",
              "type": "string"
            },
            {
              "id": "e2ac01ba-3eaa-4fd9-9b44-5f4d4fdf55c6",
              "name": "page_json",
              "value": "={{ JSON.stringify($('HTTP_Fetch_PageJson').item.json) }}",
              "type": "string"
            },
            {
              "id": "7950a617-f19b-48d8-8295-38adbfe40949",
              "name": "year_span",
              "value": "={{ $('SET_Input_Init').item.json.year_span }}",
              "type": "string"
            },
            {
              "id": "4486d299-3579-4aeb-8a12-87ba5114c1ca",
              "name": "_id",
              "value": "={{ $('SET_Input_Init').item.json._id }}",
              "type": "string"
            },
            {
              "id": "22af66de-ab19-4755-863a-76863e291576",
              "name": "row_spalte_4",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_4 }}",
              "type": "string"
            },
            {
              "id": "88a77235-d9b3-44cb-9ac8-31b14e602660",
              "name": "row_spalte_5",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_5 }}",
              "type": "string"
            },
            {
              "id": "7f07962e-bb35-4acc-9392-f98c385e25ab",
              "name": "row_spalte_6",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_6 }}",
              "type": "string"
            },
            {
              "id": "5a275ae5-34d2-4a65-b1c3-ecf1c13a14a4",
              "name": "row_spalte_7",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_7 }}",
              "type": "string"
            },
            {
              "id": "90e43839-0634-4679-bf8a-5ce7bb04be93",
              "name": "row_spalte_8",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_8 }}",
              "type": "string"
            },
            {
              "id": "708e9e8e-6d59-432a-b3cc-1b39aa726ed8",
              "name": "row_spalte_9",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_9 }}",
              "type": "string"
            },
            {
              "id": "6ae688d3-0eab-47e7-b5bb-755480f8f02d",
              "name": "row_spalte_10",
              "value": "={{ $('HTTP_Fetch_RowJson').item.json.spalte_10 }}",
              "type": "string"
            },
            {
              "id": "f2cfb1fe-22b7-41f4-b482-7915d64966ac",
              "name": "previous_rows_context_string",
              "value": "={{ $('SET_Input_Init').item.json.previous_rows_context_string }}",
              "type": "string"
            },
            {
              "id": "acf763f7-88dc-43ad-9c24-5121ecf028cd",
              "name": "currentItem.previous_row_corrected",
              "value": "={{ $('SET_Input_Init').item.json.currentItem.previous_row_corrected }}",
              "type": "string"
            },
            {
              "id": "b739f51e-f18d-4996-aa43-e21e02b8efba",
              "name": "config_sektion_zwei",
              "value": "={{(() => {\n  // 1. Config laden (robust gegen String/Objekt Unterschiede)\n  // Passe 'Edit Fields3' an den Namen deines Config-Nodes an!\n  const raw = $('SET_Input_Init').item.json.config; \n  let cfg = {};\n  \n  if (raw && typeof raw === 'object') {\n    cfg = raw;\n  } else {\n    const s = String(raw || '');\n    const a = s.indexOf('{');\n    const b = s.lastIndexOf('}');\n    if (a >= 0 && b > a) {\n      try { \n        cfg = JSON.parse(s.slice(a, b + 1)); \n      } catch (e) { \n        cfg = { _parsing_error: e.message }; \n      }\n    }\n  }\n\n  // 2. Spaltenanzahl dynamisch ermitteln\n  let n = cfg?.layout_regeln?.spaltenanzahl ?? cfg?.spaltenanzahl ?? 10;\n  n = parseInt(n, 10);\n  if (!Number.isFinite(n)) n = 10;\n  n = Math.max(1, Math.min(10, n));\n\n  // 3. Spalten-Mapping f\u00fcr das LLM aufbereiten\n  const zuw = cfg?.spalten_zuweisung || {};\n  const spalten_zuweisung_aktiv = {};\n  for (let i = 1; i <= n; i++) {\n    const k = `spalte_${i}`;\n    spalten_zuweisung_aktiv[k] = zuw[k] ?? zuw[String(i)] ?? null;\n  }\n\n  // 4. Output-Objekt bauen (Das \"compact\" Config Objekt)\n  const compact = {\n    // A) Basis-Infos f\u00fcr LLM\n    spaltenanzahl_aktiv: n,\n    spalten_zuweisung_aktiv,\n    REGELN_KURZ: [\n      `NUR spalte_1..spalte_${n} sind aktiv. spalte_${n+1}..spalte_10 MUESSEN leer bleiben.`,\n      `Spaltenreihenfolge ist fix. KEIN Spaltenversatz-Korrigieren ausser explizit gefordert.`,\n      `Werte in Datierungs-/Datumsspalten (z.B. Dopplung des Eintragsdatums) NICHT als Versatz interpretieren.`\n    ],\n    \n    // B) Legacy-Felder (f\u00fcr bestehende Prompts)\n    datumsfenster: cfg?.kontext_regeln?.datumsfenster ?? null,\n    jahr_default: cfg?.kontext_regeln?.jahr_default ?? null,\n    primaer_inventarnummer_regex: cfg?.primaer_inventarnummer_struktur?.regex ?? null,\n    spalten_semantik_ergaenzungen: cfg?.spalten_semantik_ergaenzungen ?? [],\n\n    // C) NEU & WICHTIG: Durchreichen der neuen Sektionen f\u00fcr Synthesizer & Logic\n    cleaning_rules: cfg?.cleaning_rules ?? {},\n    kontext_regeln: cfg?.kontext_regeln ?? {},\n    buch_identifikation: cfg?.buch_identifikation ?? {},\n    sonderfaelle_und_annotationen: cfg?.sonderfaelle_und_annotationen ?? {},\n\n    // D) Sekund\u00e4r-Nummernkreise: als Array durchreichen (LLM3 .map() erwartet Array).\n    //    In der Config liegen sie unter sekundaer_nummernkreise.systeme.\n    sekundaer_nummernkreise: cfg?.sekundaer_nummernkreise?.systeme ?? [],\n\n    // E) Volle Spalten-Zuweisung (LLM7/Provenienz liest spalten_zuweisung direkt).\n    spalten_zuweisung: cfg?.spalten_zuweisung ?? {}\n  };\n\n  return JSON.stringify(compact, null, 2);\n})()}}",
              "type": "string"
            },
            {
              "id": "a260afaf-5fce-4ca9-b300-3a2be16e7015",
              "name": "validierung",
              "value": "={{ $('SET_Input_Init').item.json.validierung }}",
              "type": "string"
            },
            {
              "id": "8d3aacf1-8bfd-4ed7-a1cd-c7f64249c972",
              "name": "row_scan_magenta",
              "value": "={{ ($('SET_Input_Init').item.json.row_scan_magenta || '').trim() }}",
              "type": "string"
            },
            {
              "id": "row_schreiber_beschr",
              "name": "schreiber_beschreibungen",
              "value": "={{ $('SET_Input_Init').item.json.schreiber_beschreibungen }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3632,
        256
      ],
      "id": "8ef9ca64-d2cf-4d34-8bab-2852bede0417",
      "name": "SET_Input_Row"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "35d19fe7-1262-41fe-b822-5152386df572",
              "name": "Input",
              "value": "={{ $('PARSE_LLM6').item.json.Input }}",
              "type": "object"
            },
            {
              "id": "9e286a34-51ee-4dc4-9b1c-236ed82d4240",
              "name": "LLM 1",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 1'] }}",
              "type": "object"
            },
            {
              "id": "5473a188-1201-4d24-965f-7c71332cbce6",
              "name": "LLM 2",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 2'] }}",
              "type": "object"
            },
            {
              "id": "01b2b3a5-cc8e-49c6-942d-7767e0dd33bc",
              "name": "LLM 3",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 3'] }}",
              "type": "object"
            },
            {
              "id": "2d2fccb0-4e74-4e7d-94e0-aa691f73fdc8",
              "name": "LLM 4",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 4'] }}",
              "type": "object"
            },
            {
              "id": "b37c6103-9d21-4e06-8c61-783808f632b9",
              "name": "LLM 5",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 5'] }}",
              "type": "array"
            },
            {
              "id": "a310b19f-b083-45e2-a567-a1d5513bf5cd",
              "name": "LLM 6",
              "value": "={{ $('PARSE_LLM6').item.json['LLM 6'] }}",
              "type": "object"
            },
            {
              "id": "b2b01748-18c8-434c-9875-89dd617c0458",
              "name": "LLM7",
              "value": "={{ $json['LLM 7'] }}",
              "type": "object"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3952,
        3616
      ],
      "id": "0b4cf688-b23e-45ce-8ec6-02d3825ca865",
      "name": "SET_Synth_Input"
    },
    {
      "parameters": {
        "content": "## 02 \u00b7 LLM-Pipeline (7 Schritte)\n**Version:** v1.0\n\nLLM1 \u2192 LLM2 \u2192 LLM3 \u2192 LLM4 \u2192 GLiNER \u2192 GND-Filter \u2192 Entity-Split \u2192 Flow 03 (parallel) \u2192 LLM5 \u2192 LLM6 \u2192 LLM7 \u2192 Synthesizer \u2192 SeaTable",
        "height": 200,
        "width": 500,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2656,
        -96
      ],
      "id": "9453dee7-8efe-473a-bf76-65acaa5cbb74",
      "name": "INFO_Version"
    },
    {
      "parameters": {
        "operation": "update",
        "tableName": "YOUR_TABLE_NAME",
        "rowId": "={{ $('SET_Input_Row').item.json._id }}",
        "columnsUi": {
          "columnValues": [
            {
              "columnName": "processing_status",
              "columnValue": "error"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.seaTable",
      "typeVersion": 2,
      "position": [
        4752,
        3184
      ],
      "id": "8905136a-b183-4360-8eb4-9912fbd14bb6",
      "name": "DB_Mark_Error"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d8f4e646-b1cf-4565-a855-923f26d73929",
              "leftValue": "={{ $json._error }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4240,
        2976
      ],
      "id": "af763f0b-bafa-4c6a-9c32-ed7b34c6d008",
      "name": "IF_LLM5_Error"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d8f4e646-b1cf-4565-a855-923f26d73929",
              "leftValue": "={{ $json._error }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        4240,
        3568
      ],
      "id": "8b05e7a9-51a9-4e40-9ffe-b289588c60ca",
      "name": "IF_LLM7_Error"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 600000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3408,
        672
      ],
      "id": "a2eb0578-3717-484f-9361-91b6446263b3",
      "name": "HTTP_LLM1"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 600000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3408,
        1152
      ],
      "id": "599a5b45-469a-4282-bf3e-f2ae2196d2be",
      "name": "HTTP_LLM2"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_LLM2_Layout | v1.0 \u2550\u2550\u2550\n// Zweck: Baut LLM2-Request (Layout/Struktur, qwen, text-only)\n\n// LLM2: Layout-Janitor (Text-only) \u2014 qwen3.6-35b-mtp\nreturn {\n  json: {\n    model: 'qwen3.6-35b-mtp',\n    response_format: { type: 'json_object' },\n    messages: [\n      { role: 'system', content: $json.rawSystempromptText },\n      { role: 'user',   content: $json.rawPromptText }\n    ],\n    temperature: 0.1,\n    top_p: 0.8,\n    top_k: 20,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 10000,\n    reasoning_budget: 1024,\n    chat_template_kwargs: { enable_thinking: false }\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3216,
        976
      ],
      "id": "7ca03ec9-a799-4a40-9270-4fc71b37acb1",
      "name": "REQ_LLM2_Layout"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "======================================================================\nUSER-INPUT: LAYOUT-KONTEXT & ROHDATEN\n=====================================================================\n### 1. VORAUSSETZUNG (KLASSIFIZIERUNG AUS SCHRITT 1)\n{\n  \"zeilen_klassifikation_typ\": \"{{ $json['LLM 1'].zeilen_klassifikation.typ }}\"\n}\n\n### 2. BUCH-KONFIGURATION (DEIN GESETZ)\n{{ $json.Input.config_sektion_zwei }}\n\n### 2b. BUCHSPEZIFISCHE OCR-KORREKTUREN\n- Erlaubte Tausche (generisch): {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.erlaubte_ocr_korrekturen?.generisch || [\"0\u2194O\",\"1\u2194l\u2194I\",\"\u00df\u2194ss\"]) }}\n- Erlaubte Tausche (buchspezifisch): {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.erlaubte_ocr_korrekturen?.buchspezifisch || []) }}\n- Tintenfarben: {{ JSON.stringify(JSON.parse($json.Input.config_sektion_zwei).kontext_regeln?.tintenfarben || {}) }}\n\n### 3. RUNTIME-EINGABEN (ROHDATEN DER ZEILE)\n- Spalte 1: {{ $json.Input.row_spalte_1 }}\n- Spalte 2: {{ $json.Input.row_spalte_2 }}\n- Spalte 3: {{ $json.Input.row_spalte_3 }}\n- Spalte 4: {{ $json.Input.row_spalte_4 }}\n- Spalte 5: {{ $json.Input.row_spalte_5 }}\n- Spalte 6: {{ $json.Input.row_spalte_6 }}\n- Spalte 7: {{ $json.Input.row_spalte_7 }}\n- Spalte 8: {{ $json.Input.row_spalte_8 }}\n- Spalte 9: {{ $json.Input.row_spalte_9 }}\n- Spalte 10: {{ $json.Input.row_spalte_10 }}\n\n### 4. DEIN AUFTRAG\nValidierere und repariere die Spaltenstruktur. Beachte besonders die 'spalten_semantik_ergaenzungen' bzgl. Preisen und Sprache.\nGib das Ergebnis exakt in diesem JSON-Schema zur\u00fcck.\nWICHTIG: GIB NUR DAS NACKTE JSON AUS. KEIN MARKDOWN!\n\n{\n  \"_meta\": {\n    \"schema_valid\": true,\n    \"reparatur_durchgefuehrt\": false\n  },\n  \"reparatur_hinweise\": [\n    \"z.B. 'Inventarnummer aus Spalte [N] in Anker-Spalte verschoben'\"\n  ],\n  \"cleaned_spalten\": {\n    \"spalte_1\": \"...\",\n    \"spalte_2\": \"...\",\n    \"spalte_3\": \"...\",\n    \"spalte_4\": \"...\",\n    \"spalte_5\": \"...\",\n    \"spalte_6\": \"...\",\n    \"spalte_7\": \"...\",\n    \"spalte_8\": \"...\",\n    \"spalte_9\": \"...\",\n    \"spalte_10\": \"...\"\n  },\n  \"_debug_llm2\": {\n    \"fehler_flags\": [],\n    \"fehler_details\": [],\n    \"unsicherheit_score\": 0.0,\n    \"hinweise\": []\n  }\n}",
              "type": "string"
            },
            {
              "id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
              "name": "rawSystempromptText",
              "value": "======================================================================\nARCHIV-LLM - SYSTEM: AGNOSTISCHER LAYOUT-REPARATOR (THE JANITOR)\n=====================================================================\n\nI. IDENTIT\u00c4T & AUFTRAG\nDu bist der \"Layout-Normalisierer\". Deine Aufgabe ist es, strukturelle OCR-/Tabellenfehler zu bereinigen, OHNE Inhalte zu erfinden.\nDu arbeitest streng config-getrieben und protokollierst jede \u00c4nderung als Reparaturhinweis.\n\nII. DIE DOKTRIN DER AGNOSTIK (NICHT VERHANDELBAR)\nDu kennst keine festen Spaltennummern. Du leitest alles aus der im User-Prompt \u00fcbergebenen 'buch_konfiguration' ab:\n- Erlaubte Zielspalten = exakt die Keys aus buch_konfiguration.spalten_zuweisung (z.B. spalte_1..spalte_N).\n- Rollen liest du ausschlie\u00dflich aus buch_konfiguration.spalten_zuweisung (Textbeschreibungen).\n- ANKER-ROLLE: role_primaer_inventarnummer\n- PREIS-ROLLE: role_preis (falls vorhanden) ODER role_entgelt (falls vorhanden)\n- FREITEXT-ROLLE: role_freitext ODER role_freitext_admin (falls vorhanden)\n- DITO-SYMBOLE ausschlie\u00dflich aus: buch_konfiguration.kontext_regeln.wiederholungs_logik.symbole\n- Wenn vorhanden: Literal-/Entf\u00e4llt-Symbole aus buch_konfiguration.kontext_regeln.entfaellt_logik.symbole (niemals l\u00f6schen/niemals DITO)\n\nWICHTIG:\n- Du interpretierst NICHT semantisch (\"Motiv->Ort\" usw.). Du machst keine Weltwissen-Korrekturen.\n- Du l\u00f6schst keine sichtbaren Zeichen.\n- Du erzeugst keine neuen Spalten au\u00dferhalb der erlaubten Zielspalten.\n\nIII. INVARIANTEN (HARTREGELN)\nA) KEIN SPALTEN-DRIFT / KEINE EXTRA-SPALTEN\n- Output cleaned_spalten darf NUR Keys enthalten, die in buch_konfiguration.spalten_zuweisung vorkommen.\n- Wenn Input zus\u00e4tzliche Keys hat (z.B. row_spalte_10 oder sonstige unbekannte Keys):\n  -> deren Inhalt wird NICHT als eigene Spalte ausgegeben,\n  -> sondern als \u00dcberhang an die FREITEXT-ROLLE angeh\u00e4ngt (mit Pr\u00e4fix \"[\u00dcberhang aus <key>]: ...\"),\n  -> und als reparatur_hinweis protokolliert.\n\nB) LEERHEIT\n- Leer ist ausschlie\u00dflich: null oder \"\".\n- Du darfst den String \"null\" niemals ausgeben.\n- Wenn etwas unverst\u00e4ndlich ist, lasse es lieber stehen und logge Unsicherheit; l\u00f6sche es nicht.\n\nC) SYMBOLERHALT (CONFIG-GETRIEBEN)\n- Symbole, die NICHT DITO sind, bleiben literal erhalten.\n- Wenn entfaellt_logik.symbole existiert: diese Symbole sind immer sichtbarer Inhalt und werden niemals zu \"\" oder [DITO].\n\nIV. REPARATUR-LOGIK (PRIORISIERT)\n1) DITO-ERKENNUNG (ANTI-HALLUZINATION) [H\u00d6CHSTE PRIORIT\u00c4T]\n- Trigger: Nur Symbole aus kontext_regeln.wiederholungs_logik.symbole.\n- Aktion: Wenn eine Zelle nach Trim nur aus genau einem DITO-Symbol besteht, setze den Zellinhalt strikt auf: [DITO]\n- Verbot: Niemals echten Inhalt aus Vorzeilen einsetzen!\n\n2) SCHEMA-GUARD & \u00dcBERHANG-MERGE\n- Baue cleaned_spalten ausschlie\u00dflich mit den erlaubten Zielspalten-Keys aus der Config.\n\n3) ANKER-REPARATUR\n- Voraussetzung: Der Zeilentyp (aus vorherigem Schritt) ist NICHT \"FORTSETZUNG\".\n- Wenn eine Inventarnummer eindeutig in einer falschen Rolle steht, kopiere sie in die ANKER-SPALTE.\n\n4) PREIS-BEREINIGUNG (REIHENFOLGE ZWINGEND)\nSchritt 1 \u2013 DATUM-CHECK ZUERST:\n  Pr\u00fcfe den Rohwert der Preis-Spalte auf Datumsmuster:\n  [1-3 Ziffern].[1-2 Ziffern].[2-4 Ziffern]\n  Beispiele: \"3.8.70\", \"3.8.7\", \"12.10.1953\", \"3.8.\"\n  \n  Wenn Datum erkannt:\n  \u2192 Wert unver\u00e4ndert in role_freitext_admin verschieben\n  \u2192 Preis-Spalte = leer\n  \u2192 STOPP \u2013 keine weitere Bereinigung f\u00fcr diesen Wert\n\nSchritt 2 \u2013 W\u00c4HRUNGSBEREINIGUNG:\n  Nur wenn kein Datum erkannt wurde:\n  \u2192 Dezimaltrennzeichen normieren\n  \u2192 Trailing-Punkte/Bindestriche entfernen\n\n5) OCR-MIKROKORREKTUR (EXTREM STRENG)\nErlaubt sind AUSSCHLIESSLICH diese drei Tausche:\n  0 \u2194 O  (Null vs. Buchstabe O)\n  1 \u2194 l  \u2194 I  (Eins vs. kleines L vs. gro\u00dfes i)\n  \u00df \u2194 ss\n\nVERBOTEN \u2013 auch wenn der Text unbekannt oder falsch klingt:\n  - Personennamen korrigieren (\"B\u00e4derff\" bleibt \"B\u00e4derff\")\n  - Ortsnamen korrigieren (\"Adviun\" bleibt \"Adviun\")\n  - Stra\u00dfennamen korrigieren\n  - Unbekannte W\u00f6rter durch bekannte ersetzen\n  - Arabische Zahlen in r\u00f6mische umwandeln (\"2\" bleibt \"2\")\n\nENTSCHEIDUNGSTEST vor jeder Korrektur:\n  W\u00e4re das Ergebnis ein anderes Wort/Name? \u2192 VERBOTEN\n  Ist es nur 0\u2194O oder 1\u2194l\u2194I oder \u00df\u2194ss? \u2192 ERLAUBT\n\nV. DEBUGGING-PFLICHT & \"_debug_llm2\"\nF\u00fclle \"_debug_llm2\" NUR, bei UNKLARE_DITO_ERKENNUNG, SYMBOL_KONFLIKT_ENTFAELLT, UNKLARE_SPALTENZUWEISUNG, UEBERHANG_UNKLAR, SCHEMA_KONFLIKT oder PARTIELLER_OCR_BRUCH.\nWenn alles klar ist, setze \"_debug_llm2\" auf {}.\n\nVI. OUTPUT-FORMAT (ABSOLUTES GESETZ)\nAntworte AUSSCHLIESSLICH im puren JSON-Format.\nVerwende KEINERLEI Markdown-Formatierung (kein ```json ... ```). \nGeneriere keinen Begleittext vor oder nach dem JSON-Objekt.",
              "type": "string"
            },
            {
              "id": "e330cf53-5f13-4a48-b607-72052276d414",
              "name": "row_scan_context",
              "value": "={{ $json.Input.row_scan_context }}",
              "type": "string"
            },
            {
              "id": "d992aa74-1812-49fd-8c1a-c1674cf772bc",
              "name": "page_scan",
              "value": "={{ $json.Input.page_scan }}",
              "type": "string"
            },
            {
              "id": "bc119282-b1bd-4d2a-957a-7f0005c86ce9",
              "name": "page_scan_minus_1",
              "value": "={{ $json.Input.page_scan_minus_1 }}",
              "type": "string"
            },
            {
              "id": "9fd00e7f-86a7-4171-86df-fa149bce1adc",
              "name": "page_scan_minus_2",
              "value": "={{ $json.Input.page_scan_minus_2 }}",
              "type": "string"
            },
            {
              "id": "9ec316ce-f07e-463f-bfa8-60d745989137",
              "name": "page_scan_minus_3",
              "value": "={{ $json.Input.page_scan_minus_3 }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2960,
        976
      ],
      "id": "929cd0f4-6a19-4830-8f38-4d678a020569",
      "name": "SET_LLM2_Prompt"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 600000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3424,
        1648
      ],
      "id": "3b582c4b-e637-4018-a745-71cfe1f03805",
      "name": "HTTP_LLM3"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 600000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3408,
        2080
      ],
      "id": "342644e3-ee97-47f1-b42e-bb3bbb5728c1",
      "name": "HTTP_LLM4"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 600000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3360,
        3424
      ],
      "id": "916e21b0-2bc3-47d0-a7ab-80ac9422d3be",
      "name": "HTTP_LLM6"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 600000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3424,
        3824
      ],
      "id": "5f570fdb-c2ee-4a3e-9e6b-878f4e58bc4e",
      "name": "HTTP_LLM7"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_GLINER_ENDPOINT/extract",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.gliner2_request }}",
        "options": {
          "timeout": 60000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        3168,
        2592
      ],
      "id": "177c3b8e-2ea4-44c5-bf2e-9230a46d1139",
      "name": "HTTP_GLiNER"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c8e7bf0a-9314-4c63-a4cc-fdbc236e31e4",
              "name": "rawPromptText",
              "value": "=Hier ist die Entit\u00e4tenliste aus dem aktuellen Inventareintrag:\n\n{{ JSON.stringify($json.entitaeten_array) }}\n\nWende die GND-Filterregeln aus dem System-Prompt an.\nGib nur die GND-f\u00e4higen Entit\u00e4ten zur\u00fcck \u2014 exakt im gleichen Format wie der \nInput, ohne Felder hinzuzuf\u00fcgen oder zu entfernen.\nAntworte nur mit dem JSON-Objekt, kein erkl\u00e4render Text, keine Markdown-Fences.",
              "type": "string"
            },
            {
              "id": "c06e74dd-1e46-4c00-a165-62ba24d88fde",
              "name": "rawSystempromptText",
              "value": "=ARCHIV-LLM \u2014 SYSTEM: GND-ENTIT\u00c4TS-FILTER\n\nI. MANDAT\nDu bist ein pr\u00e4ziser Klassifikationsfilter f\u00fcr Museumsinventar-Daten.\nDu entscheidest f\u00fcr jede Entit\u00e4t, ob sie einem der offiziellen GND-Entit\u00e4tstypen \nentspricht \u2014 also ob sie grunds\u00e4tzlich als Normdatensatz in der Gemeinsamen \nNormdatei (DNB) erfasst sein k\u00f6nnte.\n\nII. GND-ENTIT\u00c4TSTYPEN \u2014 POSITIVLISTE\n\nBehalte eine Entit\u00e4t NUR wenn sie einem dieser Typen entspricht:\n\n**Person (typ: person)**\nEine real existierende oder historische nat\u00fcrliche Person.\nErkennungsmerkmale: Vor- und Nachname, ggf. mit Lebensdaten oder Berufsangabe.\nBeispiele: \u201eBachmann, Hermann\", \u201eDoelicke, Gisela\"\n\n**K\u00f6rperschaft (typ: institution)**\nEine rechtlich oder organisatorisch eigenst\u00e4ndige Gemeinschaft:\nFirmen, Galerien, Museen, Beh\u00f6rden, Vereine, Verlage, Verb\u00e4nde.\nErkennungsmerkmale: Organisationsname, oft mit Rechtsform oder institutionellem \nCharakter.\nBeispiele: \u201eGalerie Bremer\", \u201eStadtmuseum Berlin\", \u201eM\u00e4rkisches Museum\"\nKEIN K\u00f6rperschaft: Dokumentenbezeichnungen, Formularnamen, Verwaltungsvorg\u00e4nge,\nStempel oder Aktenbezeichnungen \u2014 auch wenn sie amtlich klingen.\nGegenprobe: Kann diese Entit\u00e4t Vertr\u00e4ge schlie\u00dfen oder Personal besch\u00e4ftigen? \nWenn nein \u2192 kein K\u00f6rperschaft.\n\n**Geografikum (typ: ort)**\nEin geografisch abgrenzbarer Ort mit eigenem Normdatensatz:\nSt\u00e4dte, Stadtteile, Bezirke, L\u00e4nder, Geb\u00e4ude, Parks, Stra\u00dfen.\nErkennungsmerkmale: Eigenname eines Ortes, Stadtteils oder geografischen Objekts.\nBeispiele: \u201eBerlin-Charlottenburg\", \u201eFriedenau\", \u201eTauentzienstra\u00dfe\"\nKEIN Geografikum: Postbezirksk\u00fcrzel allein (z.B. \u201eW 30\", \u201eSW 11\") ohne \neigenst\u00e4ndigen Ortsnamen.\n\n**Sachbegriff (typ: sonstige)**\nEin normierter Fachbegriff f\u00fcr Themen, Techniken, Materialien oder abstrakte \nKonzepte, der im allgemeinen Sprachgebrauch nachweisbar ist.\nErkennungsmerkmale: Gattungsbegriff, Techniknamen, Materialbeschreibungen.\nBeispiele: \u201e\u00d6lmalerei\", \u201eAquarell\", \u201eKohlezeichnung\"\n\nIII. NICHT IN DER GND \u2014 NEGATIVBEISPIELE\n\nDiese Typen haben keinen GND-Normdatensatz und m\u00fcssen herausgefiltert werden:\n- Verwaltungsvorg\u00e4nge und Formularnamen: \u201eVerm\u00f6gensverzeichniskartei\", \n  \u201eInventarbuch\", \u201eKarteikarte\", \u201eRevisionsvermerk\"\n- Inventar- und Kontrollnummern: \u201e53/XI/2-11\", \u201eXI/1\"\n- Postbezirksk\u00fcrzel ohne Ortsnamen: \u201eW 30\", \u201eBerlin W. 30\", \u201eSW 11\"\n- Ma\u00dfe, Preise, Datumsangaben\n- Stempeltext und Aktenvermerke\n\nIV. ENTSCHEIDUNGSREGEL\nPr\u00fcfe f\u00fcr jede Entit\u00e4t AUSSCHLIESSLICH diese zwei Felder:\n  - `typ` (person / ort / institution / sonstige)\n  - `name_original` (der Originaltext der Entit\u00e4t)\n\nIGNORIERE vollst\u00e4ndig: `entitaet_id`, `original_context`, `score`,\n`quelle_spalte`, `name_norm` \u2014 das sind interne Metadaten, die f\u00fcr\ndie GND-Klassifikation irrelevant sind und nicht in die Entscheidung\neinfliessen d\u00fcrfen.\n\nBeispiel: entitaet_id \"ENT_XI/1_person_1\" enth\u00e4lt \"XI/1\" \u2014 das ist\nKEIN Hinweis auf eine Inventarnummer. Es ist eine interne ID.\nDie Entit\u00e4t selbst ist typ=person, name_original=\"Bachmann, Hermann\" \u2192 BEHALTEN.\n\nWenn Typ und Name einem der vier GND-Typen entsprechen \u2192 behalten.\nSonst \u2192 entfernen.\nKein Nachschlagen, keine Internetrecherche \u2014 nur Klassifikation.\n\nV. OUTPUT\nAntworte ausschlie\u00dflich mit einem JSON-Objekt ohne Markdown-Formatierung.\n{\n  \"entitaeten_array\": [ /* nur behaltene Entit\u00e4ten, unver\u00e4ndertes Format */ ]\n}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3632,
        2352
      ],
      "id": "6f5b7e3e-467c-4b44-9843-81ebd810035f",
      "name": "SET_GNDFilter_Prompt"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 REQ_GND_Filter | v1.0 \u2550\u2550\u2550\n// Zweck: Baut phi-4-mini Filter-Request (GND-Kategorien)\n// phi4_mini \u2014 text-only, leichte Klassifikationsaufgabe\nreturn {\n  json: {\n    model: 'phi-4-mini',\n    response_format: {\n      type: 'json_schema',\n      json_schema: {\n        name: 'gnd_filter_result',\n        strict: true,\n        schema: {\n          type: 'object',\n          properties: {\n            entitaeten_array: {\n              type: 'array',\n              items: {\n                type: 'object',\n                properties: {\n                  entitaet_id:      { type: 'string' },\n                  typ:              { type: 'string' },\n                  name_original:    { type: 'string' },\n                  name_norm:        { type: 'string' },\n                  quelle_spalte:    { type: 'string' },\n                  original_context: { type: 'string' },\n                  score:            { type: 'number' }\n                },\n                required: ['entitaet_id','typ','name_original'],\n                additionalProperties: true\n              }\n            }\n          },\n          required: ['entitaeten_array'],\n          additionalProperties: false\n        }\n      }\n    },\n    messages: [\n      { role: 'system', content: $json.rawSystempromptText },\n      { role: 'user',   content: $json.rawPromptText }\n    ],\n    temperature: 0.0,\n    top_p: 0.9,\n    top_k: 10,\n    presence_penalty: 0.0,\n    frequency_penalty: 0.0,\n    max_tokens: 2048\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3824,
        2352
      ],
      "id": "258c8961-8ea6-4ead-99f4-7975efa1a58f",
      "name": "REQ_GNDFilter",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_LLM_ENDPOINT/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_API_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {
          "timeout": 60000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        4080,
        2400
      ],
      "id": "9592f071-709c-462e-97d3-3db66c3e07bd",
      "name": "HTTP_GNDFilter_LLM",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Parse_GND_Filter | v1.0 \u2550\u2550\u2550\n// Zweck: Parst phi-4-mini Filter-Response \u2192 entitaeten_array\n\nlet raw = $input.item.json.choices[0].message.content;\n\n// Markdown-Fences entfernen falls vorhanden\nraw = raw.replace(/^```json\\s*/i, '').replace(/```\\s*$/i, '').trim();\n\nconst parsed = JSON.parse(raw);\n\nreturn {\n  json: {\n    entitaeten_array: parsed.entitaeten_array || []\n  }\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4304,
        2416
      ],
      "id": "9d891cca-96f9-4a09-9250-2c64e4ad9eb5",
      "name": "PARSE_GNDFilter"
    },
    {
      "id": "fn_synthi2",
      "name": "FN_Synthesizer_T2",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3484,
        3880
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Synthesizer_T2 | v1.0 \u2550\u2550\u2550\n// Baut Tabelle 2 (Transkript): roh/final/validiert pro Spalte\n\nconst item = $input.item.json;\n\n// alle_entitaeten_json parsen f\u00fcr validiert-Feld\nlet entitaeten = [];\ntry {\n  if (item.alle_entitaeten_json) {\n    entitaeten = JSON.parse(item.alle_entitaeten_json);\n  }\n} catch(e) {}\n\n// validiert pro Spalte: Entit\u00e4ten nach quelle_spalte gruppieren\nconst validiert = {};\nfor (const e of entitaeten) {\n  const qs = e.quelle_spalte;\n  if (!qs) continue;\n  if (!validiert[qs]) validiert[qs] = [];\n  const label = e.name_norm || e.name_original || '';\n  const gnd = e.gnd_id ? ` (GND:${e.gnd_id})` : '';\n  const conf = e.confidence ? ` [${e.confidence}]` : '';\n  validiert[qs].push(`${label}${gnd}${conf}`);\n}\n\nconst row = {\n  _id: item._id,\n  inv_nr_normalisiert: item.inv_nr_normalisiert || '',\n  eintrag_durchgestrichen: item.eintrag_durchgestrichen || '',\n  qa_status: item.qa_status || '',\n};\n\nfor (let i = 1; i <= 9; i++) {\n  const k = `spalte_${i}`;\n  row[`${k}_roh`]       = item[`${k}_rohtext`] || '';\n  row[`${k}_final`]     = item[`${k}_final`] || '';\n  row[`${k}_validiert`] = validiert[k] ? validiert[k].join(' \u00b7 ') : '';\n}\n\nreturn [{ json: row }];\n"
      }
    },
    {
      "id": "fn_synthi3",
      "name": "FN_Synthesizer_T3",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3484,
        4280
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 FN_Synthesizer_T3 | v1.0 \u2550\u2550\u2550\n// Baut Tabelle 3 (Forschung): Entit\u00e4ten + Provenienz\n\nconst item = $input.item.json;\n\nconst row = {\n  _id: item._id,\n  inv_nr_normalisiert: item.inv_nr_normalisiert || '',\n};\n\n// Personen\nfor (let i = 1; i <= 3; i++) {\n  for (const f of ['name_original','name_norm','gnd_id','confidence','rolle']) {\n    row[`person_${i}_${f}`] = item[`person_${i}_${f}`] || '';\n  }\n}\n// Orte\nfor (let i = 1; i <= 3; i++) {\n  for (const f of ['name_original','name_norm','gnd_id','confidence']) {\n    row[`ort_${i}_${f}`] = item[`ort_${i}_${f}`] || '';\n  }\n}\n// Institutionen\nfor (let i = 1; i <= 2; i++) {\n  for (const f of ['name_original','name_norm','gnd_id','confidence']) {\n    row[`institution_${i}_${f}`] = item[`institution_${i}_${f}`] || '';\n  }\n}\n// Sekund\u00e4rnummern\nfor (let i = 1; i <= 4; i++) {\n  row[`sek_inv_nr_${i}_nummer`] = item[`sek_inv_nr_${i}_nummer`] || '';\n  row[`sek_inv_nr_${i}_system`] = item[`sek_inv_nr_${i}_system`] || '';\n}\n// Provenienz\nfor (const f of ['prov_typ','prov_preis_wert','prov_preis_waehrung',\n                  'prov_verkaufer_name','prov_transaktionsort',\n                  'altbestand_ist','altbestand_kategorie']) {\n  row[f] = item[f] || '';\n}\n\nreturn [{ json: row }];\n"
      }
    },
    {
      "id": "db_update_t2",
      "name": "DB_Update_Row_T2",
      "type": "n8n-nodes-base.seaTable",
      "typeVersion": 2,
      "position": [
        3784,
        3880
      ],
      "parameters": {
        "operation": "update",
        "tableName": "YOUR_TABLE2_NAME",
        "rowId": "={{ $json._id }}",
        "columnsUi": {
          "columnValues": [
            {
              "columnName": "inv_nr_normalisiert",
              "columnValue": "={{ $json.inv_nr_normalisiert }}"
            },
            {
              "columnName": "eintrag_durchgestrichen",
              "columnValue": "={{ $json.eintrag_durchgestrichen }}"
            },
            {
              "columnName": "qa_status",
              "columnValue": "={{ $json.qa_status }}"
            },
            {
              "columnName": "spalte_1_roh",
              "columnValue": "={{ $json.spalte_1_roh }}"
            },
            {
              "columnName": "spalte_1_final",
              "columnValue": "={{ $json.spalte_1_final }}"
            },
            {
              "columnName": "spalte_1_validiert",
              "columnValue": "={{ $json.spalte_1_validiert }}"
            },
            {
              "columnName": "spalte_2_roh",
              "columnValue": "={{ $json.spalte_2_roh }}"
            },
            {
              "columnName": "spalte_2_final",
              "columnValue": "={{ $json.spalte_2_final }}"
            },
            {
              "columnName": "spalte_2_validiert",
              "columnValue": "={{ $json.spalte_2_validiert }}"
            },
            {
              "columnName": "spalte_3_roh",
              "columnValue": "={{ $json.spalte_3_roh }}"
            },
            {
              "columnName": "spalte_3_final",
              "columnValue": "={{ $json.spalte_3_final }}"
            },
            {
              "columnName": "spalte_3_validiert",
              "columnValue": "={{ $json.spalte_3_validiert }}"
            },
            {
              "columnName": "spalte_4_roh",
              "columnValue": "={{ $json.spalte_4_roh }}"
            },
            {
              "columnName": "spalte_4_final",
              "columnValue": "={{ $json.spalte_4_final }}"
            },
            {
              "columnName": "spalte_4_validiert",
              "columnValue": "={{ $json.spalte_4_validiert }}"
            },
            {
              "columnName": "spalte_5_roh",
              "columnValue": "={{ $json.spalte_5_roh }}"
            },
            {
              "columnName": "spalte_5_final",
              "columnValue": "={{ $json.spalte_5_final }}"
            },
            {
              "columnName": "spalte_5_validiert",
              "columnValue": "={{ $json.spalte_5_validiert }}"
            },
            {
              "columnName": "spalte_6_roh",
              "columnValue": "={{ $json.spalte_6_roh }}"
            },
            {
              "columnName": "spalte_6_final",
              "columnValue": "={{ $json.spalte_6_final }}"
            },
            {
              "columnName": "spalte_6_validiert",
              "columnValue": "={{ $json.spalte_6_validiert }}"
            },
            {
              "columnName": "spalte_7_roh",
              "columnValue": "={{ $json.spalte_7_roh }}"
            },
            {
              "columnName": "spalte_7_final",
              "columnValue": "={{ $json.spalte_7_final }}"
            },
            {
              "columnName": "spalte_7_validiert",
              "columnValue": "={{ $json.spalte_7_validiert }}"
            },
            {
              "columnName": "spalte_8_roh",
              "columnValue": "={{ $json.spalte_8_roh }}"
            },
            {
              "columnName": "spalte_8_final",
              "columnValue": "={{ $json.spalte_8_final }}"
            },
            {
              "columnName": "spalte_8_validiert",
              "columnValue": "={{ $json.spalte_8_validiert }}"
            },
            {
              "columnName": "spalte_9_roh",
              "columnValue": "={{ $json.spalte_9_roh }}"
            },
            {
              "columnName": "spalte_9_final",
              "columnValue": "={{ $json.spalte_9_final }}"
            },
            {
              "columnName": "spalte_9_validiert",
              "columnValue": "={{ $json.spalte_9_validiert }}"
            }
          ]
        }
      }
    },
    {
      "id": "db_update_t3",
      "name": "DB_Update_Row_T3",
      "type": "n8n-nodes-base.seaTable",
      "typeVersion": 2,
      "position": [
        3784,
        4280
      ],
      "parameters": {
        "operation": "update",
        "tableName": "YOUR_TABLE3_NAME",
        "rowId": "={{ $json._id }}",
        "columnsUi": {
          "columnValues": [
            {
              "columnName": "inv_nr_normalisiert",
              "columnValue": "={{ $json.inv_nr_normalisiert }}"
            },
            {
              "columnName": "person_1_name_original",
              "columnValue": "={{ $json.person_1_name_original }}"
            },
            {
              "columnName": "person_1_name_norm",
              "columnValue": "={{ $json.person_1_name_norm }}"
            },
            {
              "columnName": "person_1_gnd_id",
              "columnValue": "={{ $json.person_1_gnd_id }}"
            },
            {
              "columnName": "person_1_confidence",
              "columnValue": "={{ $json.person_1_confidence }}"
            },
            {
              "columnName": "person_1_rolle",
              "columnValue": "={{ $json.person_1_rolle }}"
            },
            {
              "columnName": "person_2_name_original",
              "columnValue": "={{ $json.person_2_name_original }}"
            },
            {
              "columnName": "person_2_name_norm",
              "columnValue": "={{ $json.person_2_name_norm }}"
            },
            {
              "columnName": "person_2_gnd_id",
              "columnValue": "={{ $json.person_2_gnd_id }}"
            },
            {
              "columnName": "person_2_confidence",
              "columnValue": "={{ $json.person_2_confidence }}"
            },
            {
              "columnName": "person_2_rolle",
              "columnValue": "={{ $json.person_2_rolle }}"
            },
            {
              "columnName": "person_3_name_original",
              "columnValue": "={{ $json.person_3_name_original }}"
            },
            {
              "columnName": "person_3_name_norm",
              "columnValue": "={{ $json.person_3_name_norm }}"
            },
            {
              "columnName": "person_3_gnd_id",
              "columnValue": "={{ $json.person_3_gnd_id }}"
            },
            {
              "columnName": "person_3_confidence",
              "columnValue": "={{ $json.person_3_confidence }}"
            },
            {
              "columnName": "person_3_rolle",
              "columnValue": "={{ $json.person_3_rolle }}"
            },
            {
              "columnName": "ort_1_name_original",
              "columnValue": "={{ $json.ort_1_name_original }}"
            },
            {
              "columnName": "ort_1_name_norm",
              "columnValue": "={{ $json.ort_1_name_norm }}"
            },
            {
              "columnName": "ort_1_gnd_id",
              "columnValue": "={{ $json.ort_1_gnd_id }}"
            },
            {
              "columnName": "ort_1_confidence",
              "columnValue": "={{ $json.ort_1_confidence }}"
            },
            {
              "columnName": "ort_2_name_original",
              "columnValue": "={{ $json.ort_2_name_original }}"
            },
            {
              "columnName": "ort_2_name_norm",
              "columnValue": "={{ $json.ort_2_name_norm }}"
            },
            {
              "columnName": "ort_2_gnd_id",
              "columnValue": "={{ $json.ort_2_gnd_id }}"
            },
            {
              "columnName": "ort_2_confidence",
              "columnValue": "={{ $json.ort_2_confidence }}"
            },
            {
              "columnName": "ort_3_name_original",
              "columnValue": "={{ $json.ort_3_name_original }}"
            },
            {
              "columnName": "ort_3_name_norm",
              "columnValue": "={{ $json.ort_3_name_norm }}"
            },
            {
              "columnName": "ort_3_gnd_id",
              "columnValue": "={{ $json.ort_3_gnd_id }}"
            },
            {
              "columnName": "ort_3_confidence",
              "columnValue": "={{ $json.ort_3_confidence }}"
            },
            {
              "columnName": "institution_1_name_original",
              "columnValue": "={{ $json.institution_1_name_original }}"
            },
            {
              "columnName": "institution_1_name_norm",
              "columnValue": "={{ $json.institution_1_name_norm }}"
            },
            {
              "columnName": "institution_1_gnd_id",
              "columnValue": "={{ $json.institution_1_gnd_id }}"
            },
            {
              "columnName": "institution_1_confidence",
              "columnValue": "={{ $json.institution_1_confidence }}"
            },
            {
              "columnName": "institution_2_name_original",
              "columnValue": "={{ $json.institution_2_name_original }}"
            },
            {
              "columnName": "institution_2_name_norm",
              "columnValue": "={{ $json.institution_2_name_norm }}"
            },
            {
              "columnName": "institution_2_gnd_id",
              "columnValue": "={{ $json.institution_2_gnd_id }}"
            },
            {
              "columnName": "institution_2_confidence",
              "columnValue": "={{ $json.institution_2_confidence }}"
            },
            {
              "columnName": "sek_inv_nr_1_nummer",
              "columnValue": "={{ $json.sek_inv_nr_1_nummer }}"
            },
            {
              "columnName": "sek_inv_nr_1_system",
              "columnValue": "={{ $json.sek_inv_nr_1_system }}"
            },
            {
              "columnName": "sek_inv_nr_2_nummer",
              "columnValue": "={{ $json.sek_inv_nr_2_nummer }}"
            },
            {
              "columnName": "sek_inv_nr_2_system",
              "columnValue": "={{ $json.sek_inv_nr_2_system }}"
            },
            {
              "columnName": "sek_inv_nr_3_nummer",
              "columnValue": "={{ $json.sek_inv_nr_3_nummer }}"
            },
            {
              "columnName": "sek_inv_nr_3_system",
              "columnValue": "={{ $json.sek_inv_nr_3_system }}"
            },
            {
              "columnName": "sek_inv_nr_4_nummer",
              "columnValue": "={{ $json.sek_inv_nr_4_nummer }}"
            },
            {
              "columnName": "sek_inv_nr_4_system",
              "columnValue": "={{ $json.sek_inv_nr_4_system }}"
            },
            {
              "columnName": "prov_typ",
              "columnValue": "={{ $json.prov_typ }}"
            },
            {
              "columnName": "prov_preis_wert",
              "columnValue": "={{ $json.prov_preis_wert }}"
            },
            {
              "columnName": "prov_preis_waehrung",
              "columnValue": "={{ $json.prov_preis_waehrung }}"
            },
            {
              "columnName": "prov_verkaufer_name",
              "columnValue": "={{ $json.prov_verkaufer_name }}"
            },
            {
              "columnName": "prov_transaktionsort",
              "columnValue": "={{ $json.prov_transaktionsort }}"
            },
            {
              "columnName": "altbestand_ist",
              "columnValue": "={{ $json.altbestand_ist }}"
            },
            {
              "columnName": "altbestand_kategorie",
              "columnValue": "={{ $json.altbestand_kategorie }}"
            }
          ]
        }
      }
    }
  ],
  "connections": {
    "TRG_Start": {
      "main": [
        [
          {
            "node": "SET_Input_Init",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM7_Prompt": {
      "main": [
        [
          {
            "node": "REQ_LLM7_Provenienz",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_LLM7_Provenienz": {
      "main": [
        [
          {
            "node": "HTTP_LLM7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_LLM7": {
      "main": [
        [
          {
            "node": "IF_LLM7_Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM1_Prompt": {
      "main": [
        [
          {
            "node": "REQ_LLM1_Classify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_Fetch_RowJson": {
      "main": [
        [
          {
            "node": "HTTP_Fetch_PageJson",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_Fetch_PageJson": {
      "main": [
        [
          {
            "node": "SET_Input_Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_Input_Init": {
      "main": [
        [
          {
            "node": "HTTP_Fetch_RowJson",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM3_Prompt": {
      "main": [
        [
          {
            "node": "REQ_LLM3_InvNr",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM4_Prompt": {
      "main": [
        [
          {
            "node": "REQ_LLM4_Extract",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FN_Entity_Split": {
      "main": [
        [
          {
            "node": "FN_Entity_Verify_Parallel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM1_Raw": {
      "main": [
        [
          {
            "node": "PARSE_LLM1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_LLM1": {
      "main": [
        [
          {
            "node": "IF_LLM1_Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM2_Raw": {
      "main": [
        [
          {
            "node": "PARSE_LLM2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_LLM2": {
      "main": [
        [
          {
            "node": "IF_LLM2_Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM3_Raw": {
      "main": [
        [
          {
            "node": "PARSE_LLM3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_LLM3": {
      "main": [
        [
          {
            "node": "IF_LLM3_Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM4_Raw": {
      "main": [
        [
          {
            "node": "PARSE_LLM4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_LLM4": {
      "main": [
        [
          {
            "node": "IF_LLM4_Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_Entity_Input": {
      "main": [
        [
          {
            "node": "FN_Entity_Split",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM5_Raw": {
      "main": [
        [
          {
            "node": "PARSE_LLM5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM6_Raw": {
      "main": [
        [
          {
            "node": "PARSE_LLM6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_LLM5": {
      "main": [
        [
          {
            "node": "IF_LLM5_Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM6_Prompt": {
      "main": [
        [
          {
            "node": "REQ_LLM6_Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_LLM6": {
      "main": [
        [
          {
            "node": "IF_LLM6_Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM7_Carry": {
      "main": [
        [
          {
            "node": "SET_LLM7_Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DB_Update_Row": {
      "main": [
        [
          {
            "node": "DB_Update_Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FN_Synthesizer": {
      "main": [
        [
          {
            "node": "DB_Update_Row",
            "type": "main",
            "index": 0
          }
        ],
        [],
        [
          {
            "node": "FN_Synthesizer_T2",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "FN_Synthesizer_T3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_LLM1_Error": {
      "main": [
        [
          {
            "node": "DB_Mark_Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SET_LLM2_Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_LLM1_Classify": {
      "main": [
        [
          {
            "node": "HTTP_LLM1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_LLM3_InvNr": {
      "main": [
        [
          {
            "node": "HTTP_LLM3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_LLM4_Extract": {
      "main": [
        [
          {
            "node": "HTTP_LLM4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_LLM6_Audit": {
      "main": [
        [
          {
            "node": "HTTP_LLM6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FN_Entity_Verify_Parallel": {
      "main": [
        [
          {
            "node": "SET_LLM5_Raw",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_LLM2_Error": {
      "main": [
        [
          {
            "node": "DB_Mark_Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SET_LLM3_Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_LLM3_Error": {
      "main": [
        [
          {
            "node": "DB_Mark_Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SET_LLM4_Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_LLM4_Error": {
      "main": [
        [
          {
            "node": "DB_Mark_Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "REQ_GLiNER",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_LLM6_Error": {
      "main": [
        [
          {
            "node": "DB_Mark_Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SET_LLM7_Carry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_GLiNER": {
      "main": [
        [
          {
            "node": "HTTP_GLiNER",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_GLiNER": {
      "main": [
        [
          {
            "node": "SET_GNDFilter_Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FN_Load_Images": {
      "main": [
        [
          {
            "node": "SET_LLM1_Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_Input_Row": {
      "main": [
        [
          {
            "node": "FN_Load_Images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_Synth_Input": {
      "main": [
        [
          {
            "node": "FN_Synthesizer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_LLM5_Error": {
      "main": [
        [
          {
            "node": "DB_Mark_Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SET_LLM6_Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF_LLM7_Error": {
      "main": [
        [
          {
            "node": "DB_Mark_Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SET_Synth_Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_LLM1": {
      "main": [
        [
          {
            "node": "SET_LLM1_Raw",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_LLM2": {
      "main": [
        [
          {
            "node": "SET_LLM2_Raw",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_LLM2_Layout": {
      "main": [
        [
          {
            "node": "HTTP_LLM2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_LLM2_Prompt": {
      "main": [
        [
          {
            "node": "REQ_LLM2_Layout",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_LLM3": {
      "main": [
        [
          {
            "node": "SET_LLM3_Raw",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_LLM4": {
      "main": [
        [
          {
            "node": "SET_LLM4_Raw",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_LLM6": {
      "main": [
        [
          {
            "node": "SET_LLM6_Raw",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_LLM7": {
      "main": [
        [
          {
            "node": "PARSE_LLM7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_GLiNER": {
      "main": [
        [
          {
            "node": "PARSE_GLiNER",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET_GNDFilter_Prompt": {
      "main": [
        [
          {
            "node": "REQ_GNDFilter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "REQ_GNDFilter": {
      "main": [
        [
          {
            "node": "HTTP_GNDFilter_LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP_GNDFilter_LLM": {
      "main": [
        [
          {
            "node": "PARSE_GNDFilter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PARSE_GNDFilter": {
      "main": [
        [
          {
            "node": "SET_Entity_Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FN_Synthesizer_T2": {
      "main": [
        [
          {
            "node": "DB_Update_Row_T2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FN_Synthesizer_T3": {
      "main": [
        [
          {
            "node": "DB_Update_Row_T3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "6b1ebfc3-8294-4141-898e-4b05aa2913f8",
  "nodeGroups": [],
  "id": "I9nRf2Zkk0e3EnPJ",
  "tags": [
    {
      "updatedAt": "2026-06-20T18:32:20.717Z",
      "createdAt": "2026-06-20T18:32:20.717Z",
      "id": "3zvvXNaJksRAPxjO",
      "name": "latest"
    }
  ]
}