{
  "name": "08 - Touchpoint Analyzer",
  "nodes": [
    {
      "id": "a8000000-0000-0000-0000-0000000000n1",
      "name": "Overview Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -540,
        -320
      ],
      "parameters": {
        "content": "## 08 \u2014 Touchpoint Analyzer (Stage 3.2, PATCHED v3 \u2014 DETERMINISTIC-FIRST) \u2014 Business Scout Agent\n\nSource-agnostic \u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440. \u0427\u0438\u0442\u0430\u0435\u0442 approved/unique \u0438\u0437 raw_market_records, \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0432 6 \u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043e\u043a (35 \u043a\u043e\u043b\u043e\u043d\u043e\u043a).\n\n\u041f\u0410\u0422\u0427 v3 (DEC-082): \u0432\u0442\u043e\u0440\u043e\u0439 live-\u0442\u0435\u0441\u0442 \u043f\u043e\u043a\u0430\u0437\u0430\u043b primary_json=0, \u0434\u043e\u0440\u043e\u0433\u043e\u0439 \u0438 \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439 LLM ($0.159 \u0437\u0430 12 \u0437\u0430\u043f\u0438\u0441\u0435\u0439). \u0422\u0435\u043f\u0435\u0440\u044c \u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u041e-\u041f\u0415\u0420\u0412\u042b\u0419. Claude \u2014 \u041e\u041f\u0426\u0418\u041e\u041d\u0410\u041b\u042c\u041d\u041e\u0415 \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435, \u0412\u042b\u041a\u041b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e.\n\nSet Analyzer Config: analysis_mode='deterministic_first', llm_enrichment=false, max_records=12, test_mode=true.\n\n\u041b\u043e\u0433\u0438\u043a\u0430:\n- Prepare Record \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u0423\u042e \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e (det) \u0434\u043b\u044f \u041a\u0410\u0416\u0414\u041e\u0419 \u0437\u0430\u043f\u0438\u0441\u0438 \u043f\u043e \u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430\u043c \u043f\u0440\u0438\u0451\u043c\u0430.\n- LLM-\u0448\u043b\u044e\u0437 (IF Call Claude?): call_claude = (\u041d\u0415 irrelevant) \u0418 (llm_enrichment=true \u0418\u041b\u0418 deterministic_needs_llm=true).\n- \u041f\u0440\u0438 deterministic_first + llm_enrichment=false \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0434\u0443\u0442 \u0432 Build Deterministic Row \u0411\u0415\u0417 Claude ($0): irrelevant \u2192 deterministic_irrelevant_skip; \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u2192 deterministic_pre_route.\n- Claude \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 det \u043d\u0435\u0443\u0432\u0435\u0440\u0435\u043d (deterministic_needs_llm=true) \u0438\u043b\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435. \u041f\u043e\u0441\u043b\u0435 Claude: primary \u2192 repair \u2192 \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 fallback (deterministic_fallback_after_llm_fail). technical_errors \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 det \u0431\u0435\u0437 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0433\u043e route \u0438\u043b\u0438 \u0441\u0431\u043e\u0439 Sheets/API.\n\n\u0414\u043b\u044f 12-\u0437\u0430\u043f\u0438\u0441\u043d\u043e\u0433\u043e \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0430 (deterministic_first, enrichment off): Claude calls=0, repair_used=0, technical_errors=0.\n\n\u0411\u0443\u0434\u0443\u0449\u0438\u0439 \u0440\u0435\u0436\u0438\u043c: analysis_mode='llm_enriched' + llm_enrichment=true \u2192 Claude \u043e\u0431\u043e\u0433\u0430\u0449\u0430\u0435\u0442 \u043d\u0435-irrelevant \u0437\u0430\u043f\u0438\u0441\u0438, fallback \u043f\u043e-\u043f\u0440\u0435\u0436\u043d\u0435\u043c\u0443 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442.\n\nparse_method: deterministic_pre_route | deterministic_irrelevant_skip | primary_json | repaired_json | deterministic_fallback_after_llm_fail | technical_error.\n\n\u041f\u0410\u0422\u0427 v6 (DEC-087, \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u044f \u043f\u043e\u0441\u043b\u0435 C2 PARTIAL PASS): (A) \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u043a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u043c\u043f\u0442 \u0434\u043b\u044f source_candidate/review_source (Telegram/Zoon) + \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 HINTS (expected_entity_type/action/route, no_contact_safety, forbidden_phrases) \u0432 payload. (B) review_source: \u0442\u043e\u043b\u044c\u043a\u043e \u041d\u0410\u0417\u0412\u0410\u041d\u041d\u042b\u0419 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442 (competitor_name) \u2192 monitor_queue; \u043e\u0431\u043e\u0431\u0449\u0451\u043d\u043d\u044b\u0439 \u043a\u0430\u0442\u0430\u043b\u043e\u0433/\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u043e\u0442\u0437\u044b\u0432\u043e\u0432 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, Zoon \u00ab\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0435 \u0431\u0440\u043e\u043a\u0435\u0440\u044b \u0432 \u041c\u043e\u0441\u043a\u0432\u0435\u00bb) \u2192 content_idea/content_queue, competitor_strength \u226445. (C) \u0431\u0435\u0437 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0430 reason \u041d\u0415 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u00ab\u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e/\u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c/\u043f\u043e\u0437\u0432\u043e\u043d\u0438\u0442\u044c/\u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f\u00bb \u2014 \u0441\u0430\u043d\u0438\u0442\u0430\u0439\u0437\u0435\u0440 \u0432 Merge \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u0442 \u043d\u0430 \u00ab\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0443\u0447\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438\u00bb. (D) \u0437\u0430\u043f\u0440\u0435\u0442 \u043d\u0435\u043e\u0431\u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u00ab\u0441\u043f\u0440\u043e\u0441 \u0440\u0430\u0441\u0442\u0451\u0442/\u043c\u043d\u043e\u0433\u043e \u043b\u0438\u0434\u043e\u0432/\u0432\u044b\u0441\u043e\u043a\u0430\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0441\u0438\u044f\u00bb. max_tokens \u041d\u0415 \u0443\u0432\u0435\u043b\u0438\u0447\u0435\u043d. \u0426\u0435\u043b\u044c C3: primary_json\u22653/4, fallback\u22641/4, technical_errors=0.\n\n\u041f\u0410\u0422\u0427 v7 (DEC-088, C4 \u2014 \u0441\u0442\u0430\u0431\u0438\u043b\u0438\u0437\u0430\u0446\u0438\u044f JSON \u0434\u043b\u044f Telegram/source_candidate): \u0434\u043b\u044f source_candidate / social_channel / telegram-content_idea \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0421\u041f\u0415\u0426\u0418\u0410\u041b\u0418\u0417\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u0410\u042f \u0423\u041b\u042c\u0422\u0420\u0410-\u041a\u041e\u0420\u041e\u0422\u041a\u0410\u042f \u0441\u0445\u0435\u043c\u0430 \u0438\u0437 7 \u043a\u043b\u044e\u0447\u0435\u0439 (profile_name, service_type, offer_text, detected_need, reason, content_idea_score, quality_score) \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u043f\u0440\u043e\u043c\u043f\u0442 \u0438 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439 payload \u0432 Build Primary Claude Request; \u043c\u0435\u043d\u044c\u0448\u0430\u044f \u043f\u043e\u0432\u0435\u0440\u0445\u043d\u043e\u0441\u0442\u044c \u043e\u0442\u0432\u0435\u0442\u0430 \u043f\u043e\u0432\u044b\u0448\u0430\u0435\u0442 \u043d\u0430\u0434\u0451\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u0442\u0440\u043e\u0433\u043e\u0433\u043e JSON \u0438 \u0443\u0431\u0438\u0440\u0430\u0435\u0442 fallback Telegram. Repair \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0435\u043c\u0435\u0439\u0441\u0442\u0432\u0430 \u0447\u0438\u043d\u0438\u0442 \u0432 \u0442\u0435 \u0436\u0435 7 \u043a\u043b\u044e\u0447\u0435\u0439 (\u043d\u0435 \u0440\u0430\u0437\u0434\u0443\u0432\u0430\u0435\u0442 \u0434\u043e 15). Merge \u043d\u0430\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u0438 7 \u043f\u043e\u043b\u0435\u0439, \u0430 route=content_queue / entity_type=content_idea / recommended_action=create_content / lead_signal_score=1 / competitor_strength=1 / contact_public='' \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438. \u041f\u043e\u0441\u0442-merge safety assertion (Task D) \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u0443\u0435\u0442 \u044d\u0442\u043e \u0434\u043b\u044f social-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. max_tokens \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u0443\u0442\u0438 500/400 (\u041d\u0415 \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c). \u0426\u0435\u043b\u044c C4: primary_json\u22653/4, Telegram \u043d\u0435 fallback, technical_errors=0.\n\n\u041d\u0415 \u043f\u0430\u0440\u0441\u0435\u0440, \u041d\u0415 \u0441\u043a\u0440\u0435\u0439\u043f\u0438\u043d\u0433, \u041d\u0415 Apify/Firecrawl. \u0422\u043e\u043b\u044c\u043a\u043e Claude HTTP (gated). active=false.",
        "height": 700,
        "width": 660,
        "color": 4
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000n2",
      "name": "Test Instructions RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        160,
        -320
      ],
      "parameters": {
        "content": "## \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438 \u0442\u0435\u0441\u0442 (\u0432\u0440\u0443\u0447\u043d\u0443\u044e)\n\n1. \u041d\u0415 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c (active=false).\n2. \u0412\u043a\u043b\u0430\u0434\u043a\u0438: 6 \u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043e\u043a (35 \u043a\u043e\u043b\u043e\u043d\u043e\u043a) + raw_market_records (40). \u0423\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442.\n3. \u041f\u041e\u0421\u041b\u0415 \u0418\u041c\u041f\u041e\u0420\u0422\u0410 \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b\u044b (ID \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b):\n   - Claude Primary / Claude Repair \u2192 Claude API - Marketing Scout\n   - Read raw_market_records / Append to Dynamic Route Sheet \u2192 Google Sheets - Marketing Scout Service Account\n4. \u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c PASTE_SPREADSHEET_ID_HERE \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 Spreadsheet ID (2 \u043d\u043e\u0434\u044b).\n\n\u0420\u0415\u0422\u0415\u0421\u0422 DETERMINISTIC-FIRST (DEC-082, 12 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 Workflow 07):\n- analysis_mode='deterministic_first', llm_enrichment=false (\u0434\u0435\u0444\u043e\u043b\u0442).\n- \u0411\u0430\u043b\u0430\u043d\u0441 Claude \u0414\u041e \u2192 Execute Workflow \u2192 \u0431\u0430\u043b\u0430\u043d\u0441 \u041f\u041e\u0421\u041b\u0415 \u2192 docs/STAGE_3_2_TEST_RESULTS.md.\n\n\u041e\u0416\u0418\u0414\u0410\u0415\u041c\u041e (deterministic_first, \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435 \u0412\u042b\u041a\u041b):\n- Claude Primary API Request \u041d\u0415 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f. Claude Repair API Request \u041d\u0415 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.\n- Claude cost delta = $0. repair_used=false \u0434\u043b\u044f \u0432\u0441\u0435\u0445 12.\n- parse_method: deterministic_pre_route = 10, deterministic_irrelevant_skip = 2.\n- technical_errors = 0.\n\n\u041e\u0436\u0438\u0434\u0430\u0435\u043c\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f:\n- 1,2 Avito competitor \u2192 monitor_queue (deterministic_pre_route).\n- 3,4 Dzen competitor channel/post \u2192 monitor_queue.\n- 5 VK source_candidate \u2192 content_queue.\n- 7,8 Telegram source_candidate \u2192 content_queue.\n- 6 Yandex Maps reviews (competitor_related) \u2192 monitor_queue.\n- 12 Zoon reviews (competitor_related) \u2192 monitor_queue.\n- 9,10 irrelevant \u2192 skipped_log (deterministic_irrelevant_skip, $0).\n- 11 Banki \u0444\u043e\u0440\u0443\u043c (hot pattern, \u0431\u0435\u0437 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0430) \u2192 review_queue, entity_type=lead_signal, recommended_action=investigate, lead_signal_score=75, needs_manual_review=true (deterministic_pre_route).\n\n\u0411\u0443\u0434\u0443\u0449\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u044f: \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c analysis_mode='llm_enriched' + llm_enrichment=true \u2192 Claude \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043d\u0435-irrelevant \u0437\u0430\u043f\u0438\u0441\u044f\u043c, fallback \u043f\u043e det \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f.",
        "height": 700,
        "width": 640,
        "color": 4
      }
    },
    {
      "id": "a8000000-0000-0000-0000-000000000002",
      "name": "Manual Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        120,
        320
      ],
      "parameters": {}
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000cf",
      "name": "Set Analyzer Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        320,
        320
      ],
      "parameters": {
        "jsCode": "// Analyzer config. deterministic_first by default, LLM enrichment OFF. No external calls by default.\n// MSK time (DEC-083): operational timestamps + run_id use explicit Moscow time +03:00.\nfunction pad(n){return String(n).padStart(2,'0');}\nfunction moscowIsoNow(){const m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction moscowStamp(){const m=new Date(Date.now()+10800000);return m.getUTCFullYear()+pad(m.getUTCMonth()+1)+pad(m.getUTCDate())+'_'+pad(m.getUTCHours())+pad(m.getUTCMinutes())+pad(m.getUTCSeconds());}\nconst now=moscowIsoNow();\nconst stamp=moscowStamp();\nvar __cfg = {\n  llm_enabled:false,                 // MASTER kill switch (DEC-119). false => NO Claude call (safe offline default).\n  analysis_mode:'llm_primary',       // \u00a72.1 PRODUCTION target mode. Claude is the PRIMARY semantic classifier for\n                                     // substantive live records (may override WF09/WF11 hints from POST_EVIDENCE).\n                                     // Deterministic logic = Stage A pre-gate + dedup + system-event + hard-negative +\n                                     // safety + freshness + fallback + free fixture/offline classifier (semantic_core).\n                                     // Runs only when llm_enabled=true (operator approval). Until then the deterministic\n                                     // offline classifier (evidence-based, taxonomy-correct) produces semantic-v2 rows.\n  data_mode:'fixture',               // \u00a713 fixture | live | manual_test \u2014 set to the mode you actually run.\n  taxonomy_version:'semantic-v2.0',\n  semantic_schema_version:'semantic-v2',\n  freshness_window_days:30,\n  llm_enrichment:false,\n  llm_enrichment_test_mode:false,\n  llm_test_batch_indexes:[1,7,11,12],\n  test_mode:true,\n  max_records:12,\n  analyze_statuses:['approved','new'],\n  production_statuses:['approved'],\n  agent_request_id_filter:'',\n  source_run_id_filter:'',            // \u00a72.4 exact source-run isolation (canonical join key)\n  force_reprocess:false,              // \u00a72.4 exactly-once: re-analyze an already-analyzed source_record only when true\n  require_quality_eligible:true,      // \u00a72.6 record-level gate: degraded/quarantined/pending never reach analysis\n  analysis_run_id:'analysis_'+stamp,  // one analyzer execution id (lineage)\n  require_llm_approval:true,          // \u00a71 runtime guard: an explicit token is required to call Claude\n  llm_approval_token:'',              // operator pastes the approved token here for an LLM run (value never logged)\n  expected_llm_approval_token:'WF08_LLM_APPROVED',\n  llm_budget_usd:0.50,                // \u00a71 hard budget; a per-call estimate above this blocks the call\n  estimated_llm_cost_per_call_usd:0.01,\n  request_cancelled:false,            // \u00a71 a cancelled request never calls Claude\n  platform_filter:'',\n  source_type_filter:'',\n  run_id:'touchpoint_'+stamp,\n  generated_at:now,\n  note:'COST-CONTROL MODE (DEC-119): llm_enabled=false is the MASTER kill switch \u2014 when false, NO record reaches Claude nodes regardless of llm_enrichment / llm_enrichment_test_mode / deterministic_needs_llm; uncertain records route to review_queue with parse_method=deterministic_uncertain_no_llm, estimated_analysis_cost_usd=0. Set llm_enabled=true (explicit operator approval) to re-enable the existing LLM path. deterministic_first + llm_enrichment=false: obvious records route WITHOUT Claude (deterministic_pre_route / deterministic_irrelevant_skip). Only deterministic_needs_llm=true records call Claude. Future mode: analysis_mode=llm_enriched + llm_enrichment=true enriches non-irrelevant records (deterministic fallback still applies). LLM 4-record test: set llm_enrichment_test_mode=true (sends only llm_test_batch_indexes through Claude). Source handoff (Stage 3.3): set agent_request_id_filter/platform_filter/source_type_filter (empty by default) to process only rows from one connector run; these apply BEFORE max_records and BEFORE per-record processing, and are independent of llm_test_batch_indexes (do NOT use batch indexes for source handoff). production: set test_mode=false -> only approved analyzed and irrelevant excluded.'\n};\n\nvar __agentIn=(typeof $json==='object'&&$json)?$json:{};\nfunction __ov(ck,ik){var k=ik||ck;var v=__agentIn[k];if(v!==undefined&&v!==null&&v!=='')__cfg[ck]=v;}\n__ov('agent_request_id_filter','agent_request_id');\n__ov('source_run_id_filter','source_run_id');\n__ov('data_mode','data_mode');\n__ov('force_reprocess','force_reprocess');\n__ov('platform_filter','platform_filter');\n__ov('source_type_filter','source_type_filter');\n__ov('llm_enabled','llm_enabled');\n__ov('llm_approval_token','llm_approval_token');\n__ov('llm_budget_usd','llm_budget_usd');\n__ov('request_cancelled','request_cancelled');\n__ov('max_records','max_records');\n// WF08-LLMAGENT-001: executeWorkflow inputs arrive as STRINGS; coerce boolean/number overrides so the\n// strict guards (cfg.llm_enabled===true) see real booleans. 'false'/'' stay false \u2014 manual defaults unchanged.\nfunction __boolish(v){return v===true||String(v).toLowerCase()==='true';}\n__cfg.llm_enabled=__boolish(__cfg.llm_enabled);\n__cfg.force_reprocess=__boolish(__cfg.force_reprocess);\n__cfg.request_cancelled=__boolish(__cfg.request_cancelled);\n__cfg.llm_budget_usd=Number(__cfg.llm_budget_usd)||0;\nreturn [{ json: __cfg }];"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000rd",
      "name": "Read raw_market_records",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        540,
        320
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "raw_market_records",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000fl",
      "name": "Filter & Select Records",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        760,
        320
      ],
      "parameters": {
        "jsCode": "// Select records to analyze. dedup_status=unique + approval_status in allowed. test_mode includes irrelevant.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\n// SCOPE-STRICT-001: accept the whole request run family (see WF16).\nfunction runFamilyMatch(rid,f){rid=String(rid==null?'':rid).trim();f=String(f==null?'':f).trim();if(!f)return true;if(!rid)return false;return rid===f||rid.indexOf(f+'::')===0||f.indexOf(rid+'::')===0;}\nconst cfg=$('Set Analyzer Config').first().json;\nconst test_mode=(cfg.test_mode===true);\nconst allowed=(test_mode?(cfg.analyze_statuses||['approved','new']):(cfg.production_statuses||['approved'])).map(s=>String(s).toLowerCase());\nconst include_irrelevant=test_mode;\nconst max_records=parseInt(cfg.max_records)||12;\nconst run_id=cfg.run_id||'';\n// Source handoff filters (Stage 3.3, DEC-091): empty by default = unchanged behavior. Applied BEFORE max_records\n// and BEFORE per-record processing so only rows from one connector run (e.g. a Workflow 09 Avito request) flow.\nconst agent_request_id_filter=str(cfg.agent_request_id_filter);\nconst source_run_id_filter=str(cfg.source_run_id_filter);\nconst force_reprocess=(cfg.force_reprocess===true);\nconst require_quality=(cfg.require_quality_eligible!==false);\nfunction canonId(x){return str(x.source_run_id)||str(x.run_id)||str(x.agent_request_id);}\n// \u00a72.4 exactly-once: keys of source records already analyzed (from the analysis_runs ledger).\nconst analyzedKeys=(function(){var m={};try{$('Read analysis_runs').all().map(function(i){return i.json;}).forEach(function(g){var k=str(g.analysis_idempotency_key)||(str(g.source_run_id)+'::'+str(g.source_record_id));if(k)m[k]=1;});}catch(e){}return m;})();\nconst platform_filter=low(cfg.platform_filter);\nconst source_type_filter=low(cfg.source_type_filter);\nconst rows=$('Read raw_market_records').all().map(i=>i.json).filter(Boolean);\nconst out=[];\nfor(const r of rows){\n  if(!str(r.record_id))continue;\n  if(low(r.dedup_status)!=='unique')continue;\n  if(!allowed.includes(low(r.approval_status)))continue;\n  const rth=low(r.record_type_hint)||'unknown';\n  if(rth==='irrelevant'&&!include_irrelevant)continue;\n  if(agent_request_id_filter&&str(r.agent_request_id)!==agent_request_id_filter)continue;\n  if(source_run_id_filter&&!runFamilyMatch(canonId(r),source_run_id_filter))continue;\n  // \u00a72.6 record-level quality gate (mixed run: each record gated on its OWN quality, not the run)\n  if(require_quality){ var __qs=low(r.quality_status); if(__qs==='degraded'||__qs==='quarantined')continue; if(low(r.review_status)==='pending')continue; if(low(r.analysis_status)==='analyzed'||low(r.analysis_status)==='done')continue; }\n  var __sid=canonId(r), __rid=str(r.source_record_id)||str(r.record_id), __key=__sid+'::'+__rid;\n  if(!force_reprocess&&analyzedKeys[__key])continue; // \u00a72.4 exactly-once\n  if(platform_filter&&low(r.platform)!==platform_filter)continue;\n  if(source_type_filter&&low(r.source_type)!==source_type_filter)continue;\n  if(out.length>=max_records)break;\n  out.push({ json: {\n    record_id:str(r.record_id), agent_request_id:str(r.agent_request_id),\n    source_run_id:__sid, source_record_id:__rid, workflow_run_id:str(r.workflow_run_id), data_mode:low(r.data_mode)||'live',\n    quality_status:low(r.quality_status), analysis_status:low(r.analysis_status)||'pending',\n    analysis_idempotency_key:__key, analysis_run_id:str(cfg.analysis_run_id)||run_id,\n    source_type:str(r.source_type),\n    platform:str(r.platform), source_url:str(r.source_url), post_url:str(r.post_url), profile_url:str(r.profile_url),\n    profile_name:str(r.profile_name), author_handle:str(r.author_handle), published_at:str(r.published_at),\n    region_hint:str(r.region_hint), service_hint:str(r.service_hint), text_context:str(r.text_context),\n    comment_text:str(r.comment_text), contact_public:str(r.contact_public), record_type_hint:rth,\n    touchpoint_type:low(r.touchpoint_type), lead_intent_hint:low(r.lead_intent_hint), urgency_hint:low(r.urgency_hint),\n    lead_temperature:low(r.lead_temperature), competitor_related:r.competitor_related, competitor_name:str(r.competitor_name),\n    interest_topic:str(r.interest_topic), probable_need:str(r.probable_need),\n    run_id:run_id, batch_index:out.length+1\n  }});\n}\n// C2 fix (DEC-086): when llm_enrichment_test_mode=true, pre-filter the SELECTED records to llm_test_batch_indexes\n// BEFORE the processing loop, so the loop receives exactly the test fixtures (e.g. [1,7,11,12] -> 4 items).\n// batch_index was assigned over the FULL selection above, so indexes stay stable (1,7,11,12). This avoids any\n// empty return inside Build Deterministic Row, which would stall Split-in-Batches loop continuation.\nconst llm_test=(cfg.llm_enrichment_test_mode===true);\nconst testIdx=Array.isArray(cfg.llm_test_batch_indexes)?cfg.llm_test_batch_indexes.map(Number):[];\nconst before=out.length;\nlet selected=out;\nif(llm_test&&testIdx.length>0){\n  selected=out.filter(it=>testIdx.indexOf(Number(it.json.batch_index))>=0);\n}\nconst after=selected.length;\nfor(const it of selected){\n  it.json.selected_count_before_test_filter=before;\n  it.json.selected_count_after_test_filter=after;\n  it.json.llm_enrichment_test_mode=llm_test;\n  it.json.llm_test_batch_indexes=testIdx;\n}\nreturn selected;"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000lp",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        980,
        320
      ],
      "parameters": {
        "options": {}
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000pr",
      "name": "Prepare Record",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        440
      ],
      "parameters": {
        "jsCode": "// Per-record holder. Builds analyzer text AND a DETERMINISTIC classification (det) from intake hints.\n// deterministic_first (DEC-082): det is the PRIMARY classifier. Claude is OPTIONAL enrichment.\n// det decides (a) is_irrelevant pre-Claude, (b) deterministic_needs_llm gate, (c) the row when Claude is skipped,\n// (d) the fallback if Claude+repair fail.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\nfunction cleanContact(raw){\n  const s=String(raw||'').trim();\n  if(!s)return '';\n  if(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/.test(s))return s;\n  if(/t\\.me\\/|wa\\.me\\//i.test(s))return s;\n  if(/(^|[^A-Za-z0-9_])@[A-Za-z0-9_]{4,}/.test(s))return s;\n  const digits=s.replace(/\\D/g,'');\n  if(digits.length>=10&&digits.length<=11)return s;\n  return '';\n}\nfunction host(u){try{return new URL(str(u)).hostname.replace(/^www\\./,'');}catch(e){return '';}}\n// Avito/classified competitor ad-intelligence det builder (DEC-092). Parses the WF09 text_context signature\n// 'Avito \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435: <TITLE> | \u0446\u0435\u043d\u0430: <PRICE> | <LOCATION> | \u043f\u0440\u043e\u0434\u0430\u0432\u0435\u0446: <SELLER> | <DESC> | \u0437\u0430\u043f\u0440\u043e\u0441: <QUERY>'\n// to populate offer_text/terms/service_type/scores. Deterministic, no Claude. lead_signal_score=1 (competitor ad).\nfunction classifiedCompetitorDet(r,tc){\n  function s2(v){return v==null?'':String(v).trim();}\n  let body=s2(tc).replace(/^avito \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435:\\s*/i,'');\n  const parts=body.split(' | ');\n  let title='',price='',seller='',q='';\n  const leftover=[];\n  parts.forEach(function(p,idx){ const t=s2(p);\n    if(idx===0){title=t;return;}\n    if(/^\u0446\u0435\u043d\u0430:/i.test(t)){price=t.replace(/^\u0446\u0435\u043d\u0430:\\s*/i,'');}\n    else if(/^\u043f\u0440\u043e\u0434\u0430\u0432\u0435\u0446:/i.test(t)){seller=t.replace(/^\u043f\u0440\u043e\u0434\u0430\u0432\u0435\u0446:\\s*/i,'');}\n    else if(/^\u0437\u0430\u043f\u0440\u043e\u0441:/i.test(t)){q=t.replace(/^\u0437\u0430\u043f\u0440\u043e\u0441:\\s*/i,'');}\n    else leftover.push(t);\n  });\n  const loc=leftover.length?leftover[0]:'';\n  const description=leftover.slice(1).join(' | ');\n  const blob=(title+' '+description+' '+q).toLowerCase();\n  const classifiedSvc=['credit_broker','business_credit','credit_after_refusals','mortgage_refinance','unknown'];\n  let svc=String(r.service_hint||'').toLowerCase();\n  if(classifiedSvc.indexOf(svc)<0){\n    if(/\u0438\u043f\u043e\u0442\u0435\u043a|\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440|\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a/.test(blob))svc='mortgage_refinance';\n    else if(/\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430|\u0438\u043f \u0438 \u043e\u043e\u043e|\u043e\u0431\u043e\u0440\u043e\u0442\u043d|\u0442\u0435\u043d\u0434\u0435\u0440\u043d|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a|\u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442/.test(blob))svc='business_credit';\n    else if(/\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432|\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a|\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a|\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d/.test(blob))svc='credit_after_refusals';\n    else svc='credit_broker';\n  }\n  const termDefs=[['\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b'],['\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442'],['\u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443','\u0440\u0430\u0431\u043e\u0442\u0430 \u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443'],['\u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u0446\u0438','\u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u0446\u0438\u044f'],['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432'],['\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a','\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432 \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u0441 \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0430\u043c\u0438'],['\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f'],['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f'],['\u043f\u0430\u043a\u0435\u0442 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442','\u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043f\u0430\u043a\u0435\u0442\u043e\u043c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432'],['\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a','\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0438\u0435 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438'],['\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440','\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435'],['\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a','\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a\u0438']];\n  const seenT={}; const terms=[];\n  termDefs.forEach(function(d){ if(blob.indexOf(d[0])>=0 && !seenT[d[1]]){seenT[d[1]]=1;terms.push(d[1]);} });\n  const termsStr=(price?price:'')+((price&&terms.length)?'; ':'')+terms.join('; ');\n  const strong=/\u043e\u0442\u043a\u0430\u0437\u043e\u0432|\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a|\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b|\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|\u0438\u043f \u0438 \u043e\u043e\u043e|\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430|\u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a|\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440/.test(blob);\n  let cs=75; if(price)cs+=4; if(terms.length>=2)cs+=4; if(description.length>40)cs+=2; if(cs>85)cs=85;\n  let ci=35; if(price||terms.length)ci=45; if(strong)ci=(price&&terms.length)?55:50;\n  let comp=0; [title,price,seller,description,s2(r.profile_url)].forEach(function(x){if(s2(x))comp++;});\n  const qs=comp>=5?82:(comp>=4?78:(comp>=3?74:70));\n  const company=s2(r.competitor_name)||seller||s2(r.profile_name)||'';\n  const offer=title||s2(r.interest_topic);\n  const need=s2(r.probable_need);\n  const reason='Avito-\u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u0430: '+(offer||'\u043e\u0444\u0444\u0435\u0440')+(price?(' ('+price+')'):'')+(terms.length?('; '+terms.slice(0,3).join(', ')):'')+'. \u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0444\u0444\u0435\u0440, \u0446\u0435\u043d\u0443 \u0438 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0443 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0438 \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0445 \u0443\u0433\u043b\u043e\u0432 (\u043f\u043b\u043e\u0449\u0430\u0434\u043a\u0430 Avito/classifieds). \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: monitor.';\n  return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'competitor', route:'monitor_queue', recommended_action:'monitor', company_name:company, service_type:svc, region:s2(r.region_hint)||loc, competitor_strength:cs, lead_signal_score:1, content_idea_score:ci, quality_score:qs, needs_manual_review:false, status:'analyzed', detected_need:need, offer_text:offer, terms:termsStr, contact:cleanContact(r.contact_public), reason:reason };\n}\nfunction classifyDeterministic(r){\n  // \u00a72.1 Stage A / \u00a79.1 system-event pre-gate: Telegram service messages (channel rename/photo/pin) are\n  // hard-skipped to skipped_log BEFORE any Claude call ($0). Service is NOT derived from a changed channel title.\n  var _sysTxt=str(r.text_context)+' '+str(r.comment_text);\n  if(low(r.record_type_hint)==='system_event'||/channel name was changed to|\u043a\u0430\u043d\u0430\u043b \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d|(\u0441\u043c\u0435\u043d\u0438\u043b|\u0438\u0437\u043c\u0435\u043d\u0438\u043b)\\s+\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\\s+\u043a\u0430\u043d\u0430\u043b\u0430|(\u0441\u043c\u0435\u043d\u0438\u043b|\u0438\u0437\u043c\u0435\u043d\u0438\u043b|\u043e\u0431\u043d\u043e\u0432\u0438\u043b)\\s+\u0444\u043e\u0442\u043e\\s+\u043a\u0430\u043d\u0430\u043b\u0430|channel (was )?created|\u043a\u0430\u043d\u0430\u043b \u0441\u043e\u0437\u0434\u0430\u043d/i.test(_sysTxt)){\n    return { is_irrelevant:true, deterministic_needs_llm:false, entity_type:'irrelevant', route:'skipped_log', recommended_action:'ignore', company_name:'', service_type:'unknown', region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:1, quality_score:1, needs_manual_review:false, status:'skipped', detected_need:'', offer_text:'', contact:'', reason:'\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 Telegram (\u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435/\u0441\u043c\u0435\u043d\u0430 \u0444\u043e\u0442\u043e/\u0437\u0430\u043a\u0440\u0435\u043f) \u2014 hard-skip \u0434\u043e Claude, $0. \u0421\u0435\u0440\u0432\u0438\u0441 \u043d\u0435 \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0438\u0437 \u043d\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u043a\u0430\u043d\u0430\u043b\u0430.', skip_reason:'telegram_service_message', is_system_event:true };\n  }\n  const rth=low(r.record_type_hint), tpt=low(r.touchpoint_type), lit=low(r.lead_intent_hint);\n  const urg=low(r.urgency_hint), temp=low(r.lead_temperature);\n  const compRelated=(r.competitor_related===true||low(r.competitor_related)==='true');\n  const svcRaw=low(r.service_hint);\n  const text=str(r.text_context)+' '+str(r.comment_text)+' '+str(r.interest_topic)+' '+str(r.probable_need);\n  const contact=cleanContact(r.contact_public);\n  const hasContact=contact!=='';\n  const hasInsight=(str(r.comment_text)!=='');\n  const allowedSvc=['secured_auto_loan','secured_real_estate_loan','pts_loan','refinancing','mortgage_adjacent','generic_lending','credit_broker','consumer_credit'];\n  function svcNorm(def){ if(allowedSvc.indexOf(svcRaw)>=0)return svcRaw; const t=text.toLowerCase(); if(t.indexOf('\u043f\u0442\u0441')>=0||t.indexOf('pts')>=0)return 'pts_loan'; if(t.indexOf('\u0431\u0440\u043e\u043a\u0435\u0440')>=0)return 'credit_broker'; return def||'unknown'; }\n  function company(){ const c=str(r.competitor_name); if(c)return c; const p=str(r.profile_name); if(p)return p; const h=host(r.source_url||r.profile_url); if(h)return h; return ''; }\n  // 1. irrelevant -> skipped_log (pre-Claude, $0)\n  if(rth==='irrelevant'||tpt==='irrelevant_source'||tpt==='weak_market_noise'){\n    return { is_irrelevant:true, deterministic_needs_llm:false, entity_type:'irrelevant', route:'skipped_log', recommended_action:'ignore', company_name:'', service_type:'unknown', region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:1, quality_score:1, needs_manual_review:false, status:'skipped', detected_need:'', offer_text:'', contact:'' };\n  }\n  // 2. competitor activity -> monitor_queue\n  if(rth==='competitor_activity'||(compRelated&&tpt.indexOf('competitor')>=0)){\n    const tc0=str(r.text_context);\n    if(low(r.source_type)==='classified'&&low(r.platform)==='avito'&&/^avito \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435:/i.test(tc0)){ return classifiedCompetitorDet(r,tc0); }\n    const clear=/\u20bd|\u0440\u0443\u0431|\u0446\u0435\u043d\u0430|\u0441\u0442\u0430\u0432\u043a|%|\u043e\u0444\u0444\u0435\u0440|offer|\u0443\u0441\u043b\u0443\u0433|\u043f\u043e\u043c\u043e\u0449\u044c|\u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d|\u043a\u0430\u043d\u0430\u043b|\u043a\u0435\u0439\u0441|usp|\u043f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432/i.test(text);\n    return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'competitor', route:'monitor_queue', recommended_action:'monitor', company_name:company(), service_type:svcNorm('generic_lending'), region:str(r.region_hint), competitor_strength:clear?78:70, lead_signal_score:1, content_idea_score:hasInsight?30:1, quality_score:clear?80:72, needs_manual_review:false, status:'analyzed', detected_need:'', offer_text:str(r.interest_topic), contact:contact };\n  }\n  // 4. review source (handle before generic source_candidate). DEC-087: ONLY a NAMED competitor\n  // (competitor_name set) is treated as a strong competitor -> monitor_queue. A generic category/\n  // listing/review directory (e.g. a Zoon \"\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0435 \u0431\u0440\u043e\u043a\u0435\u0440\u044b \u0432 \u041c\u043e\u0441\u043a\u0432\u0435\" category page) is a CONTENT/\n  // monitoring source -> content_queue, content_idea, competitor_strength capped (<=45), no \"strong competitor\".\n  if(tpt==='review_source'){\n    const specificCompetitor=(str(r.competitor_name)!=='');\n    if(specificCompetitor){\n      return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'competitor', route:'monitor_queue', recommended_action:'monitor', company_name:company(), service_type:svcNorm('generic_lending'), region:str(r.region_hint), competitor_strength:70, lead_signal_score:1, content_idea_score:35, quality_score:68, needs_manual_review:false, status:'analyzed', detected_need:str(r.probable_need), offer_text:str(r.interest_topic), contact:contact };\n    }\n    return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'content_idea', route:'content_queue', recommended_action:'create_content', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:(compRelated?45:1), lead_signal_score:1, content_idea_score:70, quality_score:68, needs_manual_review:true, status:'analyzed', detected_need:str(r.probable_need), offer_text:str(r.interest_topic), contact:contact };\n  }\n  // 3. source candidate / market signal -> content_queue\n  if(rth==='market_signal'&&tpt==='source_candidate'){\n    return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'content_idea', route:'content_queue', recommended_action:'create_content', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:55, quality_score:52, needs_manual_review:true, status:'analyzed', detected_need:'', offer_text:str(r.interest_topic), contact:contact };\n  }\n  // 5. question / objection / hot pattern -> results (with contact) or review_queue\n  if(rth==='question_objection'||temp==='hot'||lit==='high'||urg==='high'){\n    if(hasContact){\n      return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'lead_signal', route:'results', recommended_action:'contact', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:85, content_idea_score:40, quality_score:80, needs_manual_review:false, status:'analyzed', detected_need:str(r.probable_need)||str(r.interest_topic), offer_text:str(r.interest_topic), contact:contact };\n    }\n    return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'lead_signal', route:'review_queue', recommended_action:'investigate', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:75, content_idea_score:40, quality_score:70, needs_manual_review:true, status:'analyzed', detected_need:str(r.probable_need)||str(r.interest_topic), offer_text:str(r.interest_topic), contact:contact };\n  }\n  // 6. fallback unknown -> review_queue content_idea (classifiable but uncertain -> ask Claude when available)\n  return { is_irrelevant:false, deterministic_needs_llm:true, entity_type:'content_idea', route:'review_queue', recommended_action:'investigate', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:40, quality_score:40, needs_manual_review:true, status:'analyzed', detected_need:'', offer_text:str(r.interest_topic), contact:contact };\n}\nfunction moscowIsoNow(){const m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nconst r=$json;\nconst cfg=$('Set Analyzer Config').first().json;\nconst llm_enrichment=(cfg.llm_enrichment===true);\nconst llm_test=(cfg.llm_enrichment_test_mode===true);\nconst testIdx=Array.isArray(cfg.llm_test_batch_indexes)?cfg.llm_test_batch_indexes.map(Number):[];\nconst now=moscowIsoNow();\nlet combined=str(r.text_context);\nif(str(r.comment_text))combined+=' || \u041a\u041e\u041c\u041c\u0415\u041d\u0422\u0410\u0420\u0418\u0419: '+str(r.comment_text);\nif(str(r.interest_topic))combined+=' || \u0422\u0415\u041c\u0410: '+str(r.interest_topic);\nif(str(r.probable_need))combined+=' || \u0412\u0415\u0420\u041e\u042f\u0422\u041d\u0410\u042f \u041f\u041e\u0422\u0420\u0415\u0411\u041d\u041e\u0421\u0422\u042c: '+str(r.probable_need);\nconst det=classifyDeterministic(r);\n// LLM gate (DEC-082): call Claude only for non-irrelevant records when enrichment is ON, or when det is uncertain.\n// llm_enrichment_test_mode (Part C): restrict Claude to the listed fixture batch_indexes only (e.g. [1,7,11,12]).\n// MASTER LLM KILL SWITCH (DEC-119): llm_enabled=false guarantees zero Claude calls \u2014 uncertain records\n// (deterministic_needs_llm=true) are NOT escalated to Claude; they go deterministically to review_queue\n// with parse_method=deterministic_uncertain_no_llm and estimated_analysis_cost_usd=0 (manual review).\nconst llm_enabled=(cfg.llm_enabled===true);\n// PRODUCTION LLM RUNTIME GUARD (\u00a71): the Claude node is ENABLED in JSON; a call proceeds ONLY when EVERY\n// condition holds. Any failure => deterministic path ($0). llm_enabled=false guarantees zero Claude calls.\nfunction _tru(v){return v===true||String(v).toLowerCase()==='true';}\nconst _apprReq=(cfg.require_llm_approval!==false);\nconst _apprOk=(!_apprReq)||(str(cfg.llm_approval_token)!==''&&str(cfg.llm_approval_token)===str(cfg.expected_llm_approval_token));\nconst _qualOk=(['degraded','quarantined'].indexOf(str(r.quality_status).toLowerCase())<0)&&(str(r.review_status).toLowerCase()!=='pending');\nconst _notDone=(str(r.analysis_status).toLowerCase()!=='analyzed'&&str(r.analysis_status).toLowerCase()!=='done');\nconst _notCancelled=!_tru(cfg.request_cancelled)&&!_tru(r.request_cancelled);\nconst _perCall=Number(cfg.estimated_llm_cost_per_call_usd);const _estPerCall=(isFinite(_perCall)&&_perCall>0)?_perCall:0.01;\nconst _budget=Number(cfg.llm_budget_usd);const _budgetOk=(!isFinite(_budget)||_budget<=0)?true:(_estPerCall<=_budget);\nconst _guardOk=llm_enabled&&_apprOk&&_qualOk&&_notDone&&_notCancelled&&_budgetOk;\nconst llm_guard_reason=(!llm_enabled)?'llm_disabled':(!_apprOk?'approval_token_invalid':(!_qualOk?'record_not_quality_eligible':(!_notDone?'already_analyzed':(!_notCancelled?'request_cancelled':(!_budgetOk?'over_llm_budget':'ok')))));\nlet call_claude;\nif(!_guardOk){\n  call_claude=false;\n} else if(llm_test){\n  call_claude=(!det.is_irrelevant)&&(testIdx.length>0?(testIdx.indexOf(Number(r.batch_index))>=0):true);\n} else {\n  call_claude=(!det.is_irrelevant)&&(llm_enrichment===true||det.deterministic_needs_llm===true);\n}\nconst llm_blocked_uncertain=(!_guardOk)&&(!det.is_irrelevant)&&(det.deterministic_needs_llm===true);\nreturn [{ json: Object.assign({}, r, { parsed_at:now, analyzer_text:combined.substring(0,3500), det:det, is_irrelevant:det.is_irrelevant, deterministic_needs_llm:det.deterministic_needs_llm, call_claude:call_claude, llm_enabled:llm_enabled, llm_guard_ok:_guardOk, llm_guard_reason:llm_guard_reason, estimated_llm_cost_per_call_usd:_estPerCall, llm_blocked_uncertain:llm_blocked_uncertain, analysis_mode:cfg.analysis_mode||'deterministic_first', llm_enrichment:llm_enrichment, llm_enrichment_test_mode:llm_test }) }];"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000ii",
      "name": "IF Call Claude?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1420,
        440
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "a8-if-callclaude-01",
              "leftValue": "={{ $json.call_claude }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000sk",
      "name": "Build Deterministic Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1640,
        280
      ],
      "parameters": {
        "jsCode": "// DETERMINISTIC-FIRST row builder (DEC-082). Handles BOTH:\n//  - irrelevant records -> skipped_log (parse_method=deterministic_irrelevant_skip)\n//  - obvious classifiable records routed WITHOUT Claude (parse_method=deterministic_pre_route)\n// Claude is NOT called for these rows ($0). 35 business fields on every path.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction moscowIsoNow(){const m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nconst src=$('Prepare Record').first().json;\nconst det=src.det||{};\n// --- Stage C Closure Patch 3: canonical source lineage (same chain WF16 uses to key source_health) ---\nfunction __lineage(pm, route){\n  var srid=str(src.source_run_id)||str(src.run_id)||str(src.agent_request_id);\n  var __cfg=$('Set Analyzer Config').first().json||{};\n  var dmode=(str(src.data_mode)||str(__cfg.data_mode)||'live').toLowerCase();\n  var p=String(pm||'').toLowerCase(), r=String(route||'').toLowerCase();\n  var pending=(p==='deterministic_uncertain_no_llm');\n  var review=pending?'pending':((str(src.review_status)||'confirmed').toLowerCase());\n  var wfQ=(r==='technical_errors'||r==='skipped_log'||p.indexOf('irrelevant')>=0||p==='technical_error')?'quarantined':((pending||p==='deterministic_fallback_after_llm_fail')?'degraded':'healthy');\n  var srcQ=String(src.quality_status||'').toLowerCase();\n  var rank={healthy:0,degraded:1,quarantined:2};\n  var quality=(rank[srcQ]!==undefined&&rank[srcQ]>rank[wfQ])?srcQ:wfQ;\n  var reportFalse=(src.report_eligible===false||String(src.report_eligible).toLowerCase()==='false');\n  var reportElig=!(quality==='quarantined'||pending||reportFalse);\n  return {source_run_id:srid,data_mode:dmode,quality_status:quality,review_status:review,report_eligible:reportElig,quality_flags:str(src.quality_flags)};\n}\n// Note (DEC-086): C2 test filtering happens in Filter & Select Records (pre-loop). This node never\n// returns [] -- non-target test records never reach it -- so loop continuation is never broken.\nconst now=moscowIsoNow();\nconst irrel=(det.is_irrelevant===true);\nconst llmBlocked=(src.llm_blocked_uncertain===true);\nconst parse_method=irrel?'deterministic_irrelevant_skip':(llmBlocked?'deterministic_uncertain_no_llm':'deterministic_pre_route');\nconst processing_status=irrel?'business_skip':'parsed_success';\nconst reason=irrel\n  ?'\u0417\u0430\u043f\u0438\u0441\u044c \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u0430 \u043d\u0435\u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u043e\u0439 \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u043f\u0440\u0438\u0451\u043c\u0430 (record_type_hint=irrelevant \u0438\u043b\u0438 touchpoint_type=irrelevant_source/weak_market_noise). Claude \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u2014 \u043d\u0443\u043b\u0435\u0432\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c.'\n  :(llmBlocked\n  ?'Cost-control \u0440\u0435\u0436\u0438\u043c (DEC-119): llm_enabled=false \u2014 Claude \u043e\u0442\u043a\u043b\u044e\u0447\u0451\u043d, \u043d\u0435\u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u041d\u0415 \u044d\u0441\u043a\u0430\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0432 LLM. \u0414\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435\u0443\u0432\u0435\u0440\u0435\u043d\u043d\u0430\u044f (deterministic_needs_llm=true) \u2192 review_queue, \u0440\u0443\u0447\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c. \u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437\u0430 $0. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: investigate.'\n  :(str(det.reason)?str(det.reason):'\u0414\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e \u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430\u043c \u043f\u0440\u0438\u0451\u043c\u0430 raw_market_records (deterministic_first, llm_enrichment=false): \u0437\u0430\u043f\u0438\u0441\u044c \u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u043e \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u043c\u0430 \u0431\u0435\u0437 Claude. \u041c\u0430\u0440\u0448\u0440\u0443\u0442: '+(det.route||'')+'. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: '+(det.recommended_action||'')+'. Claude \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u2014 \u043d\u0443\u043b\u0435\u0432\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c.'));\nconst __linDet=__lineage(parse_method, det.route||'skipped_log');\nreturn [{ json: {\n  created_at:now, source_type:str(src.source_type), platform:str(src.platform), source_url:str(src.source_url),\n  parsed_at:str(src.parsed_at)||now, published_at:str(src.published_at), freshness_status:'unknown',\n  entity_type:det.entity_type||'irrelevant', company_name:str(det.company_name), profile_name:str(src.profile_name), profile_url:str(src.profile_url),\n  region:str(det.region)||str(src.region_hint), service_type:det.service_type||'unknown', offer_text:str(det.offer_text), terms:str(det.terms), contact_public:str(det.contact),\n  text_context:str(src.analyzer_text||src.text_context), detected_need:str(det.detected_need),\n  competitor_strength:det.competitor_strength||1, lead_signal_score:det.lead_signal_score||1, content_idea_score:det.content_idea_score||1, quality_score:det.quality_score||1,\n  reason:reason,\n  recommended_action:det.recommended_action||'ignore', status:det.status||(irrel?'skipped':'analyzed'), processing_status:processing_status,\n  parse_method:parse_method, parse_error:'', raw_response_preview:'',\n  route:det.route||'skipped_log', needs_manual_review:(det.needs_manual_review===true), repair_used:false, repair_status:'',\n  run_id:str(src.run_id), batch_index:src.batch_index||0,\n  source_run_id:__linDet.source_run_id, data_mode:__linDet.data_mode, quality_status:__linDet.quality_status, review_status:__linDet.review_status, report_eligible:__linDet.report_eligible, quality_flags:__linDet.quality_flags\n}}];"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000bp",
      "name": "Build Primary Claude Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1640,
        600
      ],
      "parameters": {
        "jsCode": "// \u00a72.1/\u00a75 WF08 llm_primary \u2014 Claude is the PRIMARY semantic classifier (semantic-v2). POST_EVIDENCE overrides hints.\n// HARD GUARD (DEC-119): no record reaches Claude while llm_enabled=false.\nconst cfgGuard=$('Set Analyzer Config').first().json;\nif(cfgGuard.llm_enabled!==true){throw new Error('WF08 LLM guard: llm_enabled=false (cost-control) \u2014 Claude blocked. Set llm_enabled=true with explicit operator approval to run llm_primary.');}\nconst r=$('Prepare Record').first().json;\nfunction cap(s,n){return (s==null?'':String(s)).substring(0,n);}\nconst POST_EVIDENCE={ post_text:cap(r.analyzer_text||r.text_context||'',1600), post_url:r.post_url||r.source_url||'', listing_title:r.title||'', published_at:r.published_at||'' };\nconst SOURCE_METADATA={ platform:r.platform||'', source_type:r.source_type||'', channel_or_profile_name:r.profile_name||'', profile_url:r.profile_url||'' };\nconst UPSTREAM_HINTS={ record_type_hint:r.record_type_hint||'', service_hint:r.service_hint||'', touchpoint_type:r.touchpoint_type||'', _note:'WEAK PRIORS ONLY \u2014 may be wrong; POST_EVIDENCE overrides these.' };\nconst ALLOWED_ENUMS={ record_type:[\"competitor_activity\", \"market_signal\", \"audience_question\", \"audience_objection\", \"audience_complaint\", \"buying_intent\", \"lead_signal\", \"source_candidate\", \"irrelevant\", \"system_event\", \"unknown\"], entity_type:[\"competitor\", \"market_signal\", \"content_idea\", \"audience_signal\", \"lead\", \"source_candidate\", \"irrelevant\", \"system_event\", \"unknown\"], activity_subtype:[\"direct_offer\", \"owned_media_content\", \"affiliate_program\", \"product_explainer\", \"educational_offer\", \"client_case\", \"price_change\", \"service_change\", \"channel_profile\", \"other\"], service:[\"credit_brokerage\", \"credit_after_refusals\", \"credit_history_consulting\", \"pts_loan\", \"secured_financing\", \"real_estate_secured_loan\", \"business_credit\", \"mortgage_brokerage\", \"mortgage_refinance\", \"auto_credit\", \"auto_lease_refinance\", \"debt_refinancing\", \"debt_consolidation\", \"bankruptcy_advisory\", \"business_credit_rehabilitation\", \"credit_legal_consulting\", \"consumer_credit\", \"bank_guarantee\", \"unknown\"] };\nconst OUTPUT_SCHEMA={ schema_version:'semantic-v2', record_type:'', entity_type:'', activity_subtype:null, competitor_related:false, competitor_name:null, service_primary:'unknown', service_secondary:[], market_signal_subtype:null, content_topics:[], pain_tags:[], offer_text:'', offer_terms:{amount_min:null,amount_max:null,currency:'RUB',price_text:null,rate_text:null,speed_text:null,conditions:[],documents_not_required:[]}, cta:null, quoted_claims:[], negated_claims:[], hard_skip:false, skip_reason:null, confidence_score:0, confidence_reasons:[], semantic_flags:[], evidence:[] };\nconst system=\"You are the PRIMARY semantic classifier for a Russian secured-lending market-intelligence system (Moscow/MO: PTS / auto / real-estate / business collateral, brokerage, refinancing, credit history). You are NOT a text enricher: you DECIDE the semantic class of each record from its own evidence.\\\\nYou cannot browse, fetch, open or verify anything. Use ONLY the provided record.\\\\nPRIORITY OF EVIDENCE: POST_EVIDENCE (the actual post/listing text + URL) is the source of truth. SOURCE_METADATA (platform, channel/profile name) and UPSTREAM_HINTS (record_type_hint/service_hint from the connector) are WEAK PRIORS ONLY and are frequently wrong. If POST_EVIDENCE contradicts a hint, OVERRIDE the hint.\\\\nDo NOT infer an offer or a competitor from a channel title or a search query alone \\u2014 require post-level evidence (a pitch, price, terms, CTA, commission, case, or owned-media funnel).\\\\nRULES: (1) A direct competitor offer / partner-referral program / owned-media expertise funnel is record_type=competitor_activity (set activity_subtype: direct_offer / affiliate_program / owned_media_content / educational_offer / client_case / price_change / service_change), NOT market_signal \\u2014 even if broad market words (\\u0431\\u0430\\u043d\\u043a\\u0438, \\u0426\\u0411, \\u0437\\u0430\\u043a\\u043e\\u043d, \\u043f\\u0440\\u043e\\u0433\\u0440\\u0430\\u043c\\u043c\\u0430, \\u0440\\u0435\\u0444\\u0438\\u043d\\u0430\\u043d\\u0441\\u0438\\u0440\\u043e\\u0432\\u0430\\u043d\\u0438\\u0435) appear. (2) market_signal is an EXTERNAL event (rate/regulation/program/demand shift), not an owned offer. (3) A quoted or negated claim (e.g. \\u00ab\\u0423\\u0434\\u0430\\u043b\\u0438\\u043c \\u043f\\u0440\\u043e\\u0441\\u0440\\u043e\\u0447\\u043a\\u0438 \\u0437\\u0430 \\u043e\\u0434\\u0438\\u043d \\u0434\\u0435\\u043d\\u044c\\u00bb \\u2014 \\u044d\\u0442\\u043e \\u043c\\u043e\\u0448\\u0435\\u043d\\u043d\\u0438\\u0447\\u0435\\u0441\\u0442\\u0432\\u043e) is a quoted_claim/negated_claim, NEVER the author's own offer or speed claim. (4) An audience voice = audience_question / audience_objection / audience_complaint / buying_intent. (5) A channel/search/review page with no observed post = source_candidate (report_eligible later=false). (6) A Telegram service message = system_event, hard_skip=true. (7) credit secured BY real estate is a lending product (real_estate_secured_loan), NOT adjacent real-estate sale/rent.\\\\nOUTPUT: return EXACTLY one JSON object matching OUTPUT_SCHEMA. The first character must be { and the last }. No markdown, no code fences, no prose, no thinking. Use ONLY values from ALLOWED_ENUMS for record_type, entity_type, activity_subtype, service_primary and each service_secondary; if a service does not fit, use \\\\\\\"unknown\\\\\\\" (never invent an enum). Put null instead of inventing amounts, terms, dates or contacts. Never invent a public contact. For every important extracted fact (amount/rate/speed/service/competitor) add an evidence item {field, quote, source} quoting POST_EVIDENCE verbatim. confidence_score (0-100) must reflect EVIDENCE strength, not which code branch ran: cap at 20 for a placeholder title, 45 for a missing description, 30 for source-metadata-only, 20 for query-prior-only. Code maps record_type to a physical queue afterwards \\u2014 do NOT output a sheet/route name.\";\nconst user=JSON.stringify({POST_EVIDENCE:POST_EVIDENCE, SOURCE_METADATA:SOURCE_METADATA, UPSTREAM_HINTS:UPSTREAM_HINTS, ALLOWED_ENUMS:ALLOWED_ENUMS, OUTPUT_SCHEMA:OUTPUT_SCHEMA});\nreturn [{ json: { model:'claude-sonnet-4-6', max_tokens:900, temperature:0, thinking:{type:'disabled'}, system:system, messages:[{ role:'user', content:user }] } }];\n"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000c1",
      "name": "Claude Primary API Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1860,
        600
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "POST",
        "url": "https://aiprimetech.io/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ model: $json.model, max_tokens: $json.max_tokens, temperature: $json.temperature, thinking: $json.thinking, system: $json.system, messages: $json.messages }) }}",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000pp",
      "name": "Parse Primary JSON",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2080,
        600
      ],
      "parameters": {
        "jsCode": "const response=$json;\nfunction cap(s,n){return (s==null?'':String(s)).substring(0,n);}\nfunction extractBalanced(s){\n  const start=s.indexOf('{'); if(start===-1)return null;\n  let depth=0,inStr=false,esc=false;\n  for(let i=start;i<s.length;i++){const c=s[i];\n    if(inStr){ if(esc){esc=false;} else if(c==='\\\\'){esc=true;} else if(c==='\"'){inStr=false;} continue; }\n    if(c==='\"'){inStr=true;continue;}\n    if(c==='{')depth++; else if(c==='}'){depth--; if(depth===0)return s.substring(start,i+1);}\n  }\n  return null;\n}\nfunction joinText(resp){\n  if(!resp||!Array.isArray(resp.content))return {ok:false,text:'',types:'no_content_array'};\n  const types=resp.content.map(c=>(c&&c.type)||'unknown').join(',');\n  const parts=resp.content.filter(c=>c&&c.type==='text'&&typeof c.text==='string').map(c=>c.text);\n  return {ok:parts.length>0,text:parts.join('\\n').trim(),types:types};\n}\nfunction tryParse(text){\n  let txt=String(text).trim();\n  txt=txt.replace(/^```json\\s*/i,'').replace(/```\\s*$/,'').trim();\n  txt=txt.replace(/^```\\s*/,'').replace(/```\\s*$/,'').trim();\n  const norm=(x)=>x.replace(/[\u2018\u2019]/g,\"'\").replace(/[\u201c\u201d\u00ab\u00bb]/g,'\"');\n  try{return {ok:true,obj:JSON.parse(norm(txt))};}catch(e){}\n  const bal=extractBalanced(txt);\n  if(bal){try{return {ok:true,obj:JSON.parse(norm(bal))};}catch(e){}}\n  const s=txt.indexOf('{'),e2=txt.lastIndexOf('}');\n  if(s!==-1&&e2!==-1&&e2>s){const slice=txt.substring(s,e2+1);try{return {ok:true,obj:JSON.parse(norm(slice))};}catch(e){}}\n  return {ok:false};\n}\nfunction fail(err,prev){\n  return [{ json:{ parse_ok:false, parse_method:'primary_json', enrichment:null, parse_error:cap(err,300), raw_response_preview:cap(prev,500) } }];\n}\nif(response.error||response.status>=400){\n  return fail('Primary HTTP error: '+cap(JSON.stringify(response.error||response.status),300),'non_json_non_text_or_thinking_response');\n}\nconst jt=joinText(response);\nif(!jt.text){\n  // thinking/signature/tool only -> no usable text\n  return fail('Primary: no text item ('+jt.types+')','non_json_non_text_or_thinking_response');\n}\nconst rawPreview=cap(jt.text,500);\nconst res=tryParse(jt.text);\nif(!res.ok){\n  return fail('Primary: text present but JSON invalid',rawPreview);\n}\nreturn [{ json:{ parse_ok:true, parse_method:'primary_json', enrichment:res.obj, parse_error:'', raw_response_preview:rawPreview } }];"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000ip",
      "name": "IF Primary Parse OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2300,
        600
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "a8-if-parse-01",
              "leftValue": "={{ $json.parse_ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000rq",
      "name": "Build Repair Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2300,
        820
      ],
      "parameters": {
        "jsCode": "const input=$json;\nconst r=$('Prepare Record').first().json;\nconst det=r.det||{};\nfunction cap(s,n){return (s==null?'':String(s)).substring(0,n);}\nconst rawPrev=cap(input.raw_response_preview||'empty',400);\n// DEC-088 C4 (Task C): source_candidate / social channels repair into the SAME ultra-short 7-key schema \u2014\n// never inflate to the full 15-key general schema. RAW_RESPONSE is the capped sanitized preview only.\nconst tpt=String(r.touchpoint_type||'').toLowerCase();\nconst rth=String(r.record_type_hint||'').toLowerCase();\nconst plat=String(r.platform||'').toLowerCase();\nconst stype=String(r.source_type||'').toLowerCase();\nconst hasContactSc=String(det.contact||'').trim()!=='';\nconst isSourceCandidate=(rth==='market_signal'&&tpt==='source_candidate')||(stype==='social_channel')||(plat==='telegram'&&String(det.entity_type||'')==='content_idea'&&!hasContactSc);\nif(isSourceCandidate){\n  const scSystem=`You repair JSON only. Return exactly one JSON object with ONLY these keys: profile_name, service_type, offer_text, detected_need, reason, content_idea_score, quality_score. First char {, last char }. No prose. No markdown. No code fences. No thinking. Do not say \"Repairing\".\\nIf RAW_RESPONSE contains JSON, normalize it to these 7 keys; otherwise build them from RECORD. This is a public monitoring/content source \u2014 reason in Russian, max 2 sentences, monitoring framing, NOT a direct lead, no contact, no outreach. service_type in [secured_auto_loan,secured_real_estate_loan,pts_loan,refinancing,mortgage_adjacent,generic_lending,credit_broker,consumer_credit,unknown]. content_idea_score and quality_score are integers 1-100. Do NOT output company_name, route, entity_type, recommended_action, lead_signal_score or competitor_strength.`;\n  const scSchema={profile_name:\"\",service_type:\"\",offer_text:\"\",detected_need:\"\",reason:\"\",content_idea_score:55,quality_score:55};\n  const scRecord={platform:r.platform||'',profile_name:r.profile_name||'',text_context:cap(r.analyzer_text||r.text_context||'',500),interest_topic:r.interest_topic||'',service_hint:r.service_hint||''};\n  const scUser=JSON.stringify({RAW_RESPONSE_SANITIZED:rawPrev,RECORD:scRecord,OUTPUT_SCHEMA:scSchema});\n  return [{ json:{ model:'claude-sonnet-4-6', max_tokens:400, temperature:0, thinking:{type:'disabled'}, system:scSystem, messages:[{ role:'user', content:scUser }] } }];\n}\nconst repairSystem=`You repair JSON only. Return exactly one JSON object matching OUTPUT_SCHEMA. First char {, last char }. No prose. No markdown. No code fences. No comments. No thinking. Do not say \"Repairing\". Do not write \"fetching\"/\"analyzing\".\nIf RAW_RESPONSE contains valid JSON, extract and normalize it to OUTPUT_SCHEMA. If RAW_RESPONSE contains no valid JSON, build a safe JSON object from ORIGINAL_RECORD and DETERMINISTIC_ROW (prefer DETERMINISTIC_ROW values). Never invent a contact. Do not return market_signal as entity_type; use content_idea.\nKeep entity_type = HINTS.expected_entity_type and recommended_action = HINTS.expected_action. Never write any phrase in HINTS.forbidden_phrases. If HINTS.no_contact_safety is true, never instruct to reach out (no \u00ab\u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e\u00bb/\u00ab\u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u00bb/\u00ab\u043f\u043e\u0437\u0432\u043e\u043d\u0438\u0442\u044c\u00bb/\u00ab\u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f\u00bb) and say the record requires manual review. Do not claim demand growth, more leads or high conversion unless present in ORIGINAL_RECORD. Scores are integers 1-100. reason is 1-2 short Russian sentences.`;\nconst OUTPUT_SCHEMA={company_name:\"\",profile_name:\"\",region:\"\",service_type:\"\",offer_text:\"\",terms:\"\",contact_public:\"\",detected_need:\"\",reason:\"\",recommended_action:\"\",entity_type:\"\",lead_signal_score:1,competitor_strength:1,content_idea_score:1,quality_score:1};\nconst ORIGINAL_RECORD={source_type:r.source_type||'',platform:r.platform||'',record_type_hint:r.record_type_hint||'',touchpoint_type:r.touchpoint_type||'',region_hint:r.region_hint||'',service_hint:r.service_hint||'',contact_public:r.contact_public||'',text_context:cap(r.analyzer_text||r.text_context||'',500)};\nconst DETERMINISTIC_ROW={entity_type:(det.entity_type||''),recommended_action:(det.recommended_action||''),service_type:(det.service_type||'unknown'),company_name:(det.company_name||''),region:(det.region||r.region_hint||''),offer_text:(det.offer_text||''),detected_need:(det.detected_need||''),contact_public:(det.contact||''),lead_signal_score:(det.lead_signal_score||1),competitor_strength:(det.competitor_strength||1),content_idea_score:(det.content_idea_score||1),quality_score:(det.quality_score||1)};\nconst hasContactDet=String(det.contact||'').trim()!=='';\nconst forbidden=['\u0441\u043f\u0440\u043e\u0441 \u0440\u0430\u0441\u0442\u0451\u0442','\u0430\u043a\u0442\u0438\u0432\u043d\u043e \u0437\u0430\u0434\u0430\u044e\u0442 \u0432\u043e\u043f\u0440\u043e\u0441\u044b','\u043c\u043d\u043e\u0433\u043e \u043b\u0438\u0434\u043e\u0432','\u0432\u044b\u0441\u043e\u043a\u0430\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0441\u0438\u044f'];\nif(!hasContactDet){forbidden.push('\u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e','\u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c','\u043f\u043e\u0437\u0432\u043e\u043d\u0438\u0442\u044c','\u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f','\u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u0430\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043b\u0438\u0434\u043e\u0432');}\nconst HINTS={expected_entity_type:(det.entity_type||''),expected_action:(det.recommended_action||''),expected_route:(det.route||''),no_contact_safety:!hasContactDet,forbidden_phrases:forbidden};\nconst userMsg=JSON.stringify({RAW_RESPONSE_SANITIZED:rawPrev,ORIGINAL_RECORD:ORIGINAL_RECORD,DETERMINISTIC_ROW:DETERMINISTIC_ROW,HINTS:HINTS,OUTPUT_SCHEMA:OUTPUT_SCHEMA});\nreturn [{ json:{ model:'claude-sonnet-4-6', max_tokens:600, temperature:0, thinking:{type:'disabled'}, system:repairSystem, messages:[{ role:'user', content:userMsg }] } }];"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000c2",
      "name": "Claude Repair API Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2520,
        820
      ],
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "POST",
        "url": "https://aiprimetech.io/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify({ model: $json.model, max_tokens: $json.max_tokens, temperature: $json.temperature, thinking: $json.thinking, system: $json.system, messages: $json.messages }) }}",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000pr2",
      "name": "Parse Repaired JSON",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2740,
        820
      ],
      "parameters": {
        "jsCode": "const response=$json;\nconst primary=$('Parse Primary JSON').first().json;\nfunction cap(s,n){return (s==null?'':String(s)).substring(0,n);}\nfunction extractBalanced(s){\n  const start=s.indexOf('{'); if(start===-1)return null;\n  let depth=0,inStr=false,esc=false;\n  for(let i=start;i<s.length;i++){const c=s[i];\n    if(inStr){ if(esc){esc=false;} else if(c==='\\\\'){esc=true;} else if(c==='\"'){inStr=false;} continue; }\n    if(c==='\"'){inStr=true;continue;}\n    if(c==='{')depth++; else if(c==='}'){depth--; if(depth===0)return s.substring(start,i+1);}\n  }\n  return null;\n}\nfunction joinText(resp){\n  if(!resp||!Array.isArray(resp.content))return {ok:false,text:'',types:'no_content_array'};\n  const types=resp.content.map(c=>(c&&c.type)||'unknown').join(',');\n  const parts=resp.content.filter(c=>c&&c.type==='text'&&typeof c.text==='string').map(c=>c.text);\n  return {ok:parts.length>0,text:parts.join('\\n').trim(),types:types};\n}\nfunction tryParse(text){\n  let txt=String(text).trim();\n  txt=txt.replace(/^```json\\s*/i,'').replace(/```\\s*$/,'').trim();\n  txt=txt.replace(/^```\\s*/,'').replace(/```\\s*$/,'').trim();\n  const norm=(x)=>x.replace(/[\u2018\u2019]/g,\"'\").replace(/[\u201c\u201d\u00ab\u00bb]/g,'\"');\n  try{return {ok:true,obj:JSON.parse(norm(txt))};}catch(e){}\n  const bal=extractBalanced(txt);\n  if(bal){try{return {ok:true,obj:JSON.parse(norm(bal))};}catch(e){}}\n  const s=txt.indexOf('{'),e2=txt.lastIndexOf('}');\n  if(s!==-1&&e2!==-1&&e2>s){const slice=txt.substring(s,e2+1);try{return {ok:true,obj:JSON.parse(norm(slice))};}catch(e){}}\n  return {ok:false};\n}\nconst primaryErr=cap(primary.parse_error||'unknown',300);\nconst primaryPrev=cap(primary.raw_response_preview||'',500);\nfunction fallback(repairErr,prev){\n  // deterministic fallback after primary+repair failed (DEC-081/085)\n  return [{ json:{ parse_ok:false, parse_method:'deterministic_fallback_after_llm_fail', enrichment:null,\n    parse_error:cap('Primary: '+primaryErr+' | Repair: '+repairErr,800),\n    raw_response_preview:cap((prev&&prev.indexOf('{')>-1)?prev:'non_json_non_text_or_thinking_response',500) } }];\n}\nif(response.error||response.status>=400){ return fallback('Repair HTTP error: '+cap(JSON.stringify(response.error||response.status),200),primaryPrev); }\nconst jt=joinText(response);\nif(!jt.text){ return fallback('Repair: no text item ('+jt.types+')',primaryPrev); }\nconst res=tryParse(jt.text);\nif(!res.ok){ return fallback('Repair: JSON invalid',cap(jt.text,500)); }\nreturn [{ json:{ parse_ok:true, parse_method:'repaired_json', enrichment:res.obj, parse_error:'', raw_response_preview:cap(jt.text,500) } }];"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000nr",
      "name": "Merge LLM Enrichment With Deterministic Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2520,
        600
      ],
      "parameters": {
        "jsCode": "// embedded n8n/lib/error_sanitizer.js (drift-proof; test asserts equality)\n// error_sanitizer.js \u2014 what may be persisted about a FAILED record (technical_errors / skipped_log routes).\n//\n// WF04-ROUTE-002. The resilient routers persist `raw_response_preview` + `parse_error` for triage. Those strings are\n// built from a provider response, so without a gate they can carry an Authorization header, an api key, a cookie, a\n// Claude thinking block, or a customer's phone/email straight into a durable Google Sheets tab that many people can\n// open. Diagnosis needs a bounded, scrubbed EXCERPT \u2014 never the raw body.\n//\n// Keep: provider, source URL, safe error category, bounded sanitized excerpt, request/run lineage, timestamp.\n// Drop: secrets, credentials, cookies, hidden reasoning, private PII, anything past the cap.\n//\n// Embeddable: unique es*-prefixed names, no cross-lib require.\n\nfunction esStr(v) { return v == null ? '' : String(v); }\n\nvar ES_MAX_PREVIEW = 300;      // enough to recognise a failure shape; far too short to be a \"raw body\"\nvar ES_REDACTED = '[\u0441\u043a\u0440\u044b\u0442\u043e]';\n\n// Ordered: the most specific secret shapes first, then generic key/value pairs, then PII.\nvar ES_RULES = [\n  // Authorization / bearer / api-key headers (with or without a header name)\n  { re: /(authorization|proxy-authorization)\\s*[:=]\\s*\\S+/gi, to: '$1: ' + ES_REDACTED },\n  { re: /\\bbearer\\s+[A-Za-z0-9._\\-~+/]{8,}=*/gi, to: 'bearer ' + ES_REDACTED },\n  { re: /\\bbasic\\s+[A-Za-z0-9+/]{8,}=*/gi, to: 'basic ' + ES_REDACTED },\n  // cookies / set-cookie\n  { re: /(set-cookie|cookie)\\s*[:=]\\s*[^\\n;]+/gi, to: '$1: ' + ES_REDACTED },\n  // provider key formats (Anthropic sk-ant-\u2026, generic sk-\u2026, Google AIza\u2026, GitHub gh[pousr]_\u2026)\n  { re: /\\bsk-ant-[A-Za-z0-9._\\-]{8,}/g, to: ES_REDACTED },\n  { re: /\\bsk-[A-Za-z0-9._\\-]{16,}/g, to: ES_REDACTED },\n  { re: /\\bAIza[0-9A-Za-z._\\-]{20,}/g, to: ES_REDACTED },\n  { re: /\\bgh[pousr]_[A-Za-z0-9]{20,}/g, to: ES_REDACTED },\n  // JWTs\n  { re: /\\beyJ[A-Za-z0-9._\\-]{10,}\\.[A-Za-z0-9._\\-]{10,}\\.[A-Za-z0-9._\\-]{4,}/g, to: ES_REDACTED },\n  // generic \"<something>key|token|secret|password\" : \"<value>\" (JSON or header style)\n  { re: /(\"?\\b[\\w.\\-]*(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|passwd|pwd|credential)\\b\"?)\\s*[:=]\\s*\"?[^\"\\s,}{&]+\"?/gi, to: '$1: ' + ES_REDACTED },\n  // url query secrets: ?key=\u2026 &token=\u2026\n  { re: /([?&](?:key|token|api_key|apikey|access_token|secret|password)=)[^&\\s]+/gi, to: '$1' + ES_REDACTED },\n  // private PII \u2014 we analyze public positioning, never harvest contacts\n  { re: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g, to: '[email]' },\n  { re: /\\+?\\d[\\d\\s().-]{9,}\\d/g, to: '[\u0442\u0435\u043b]' }\n];\n\n// Strip a model's hidden reasoning: it is never persisted, in any shape.\nfunction esStripThinking(s) {\n  return esStr(s)\n    .replace(/<thinking>[\\s\\S]*?<\\/thinking>/gi, ' ')\n    .replace(/<thinking>[\\s\\S]*?<\\/antml:thinking>/gi, ' ')\n    .replace(/\"type\"\\s*:\\s*\"thinking\"[\\s\\S]*?(?=[,}]\\s*\"type\"|$)/gi, ' ')\n    .replace(/\\bthinking\\s*[:=]\\s*\"[^\"]*\"/gi, ' ');\n}\n\n// sanitizeErrorPreview(raw, opts) -> a bounded, secret-free, PII-free excerpt safe to persist.\nfunction sanitizeErrorPreview(raw, opts) {\n  opts = opts || {};\n  var max = Number(opts.max_chars);\n  if (!isFinite(max) || max <= 0) max = ES_MAX_PREVIEW;\n  var s = esStripThinking(raw);\n  for (var i = 0; i < ES_RULES.length; i++) s = s.replace(ES_RULES[i].re, ES_RULES[i].to);\n  s = s.replace(/\\s+/g, ' ').trim();\n  if (s.length > max) s = s.slice(0, max - 1) + '\u2026';\n  return s;\n}\n\n// Does a string still look like it carries a secret? Used as a fail-closed assertion before persisting.\nfunction esLooksSecret(s) {\n  s = esStr(s);\n  return /\\b(sk-ant-|sk-[A-Za-z0-9]{16,}|AIza[0-9A-Za-z]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|eyJ[A-Za-z0-9._-]{10,}\\.)/.test(s) ||\n    /(authorization|set-cookie)\\s*[:=]\\s*(?!\\[\u0441\u043a\u0440\u044b\u0442\u043e\\])\\S+/i.test(s);\n}\n\n// The ONLY diagnostic fields a failed record may persist. Anything not listed here is dropped by construction.\nfunction sanitizeErrorRecord(rec, opts) {\n  rec = rec || {};\n  return {\n    provider: esStr(rec.provider),\n    source_url: esStr(rec.source_url),\n    error_category: esStr(rec.error_category || rec.processing_status),\n    parse_error: sanitizeErrorPreview(rec.parse_error, { max_chars: (opts && opts.max_chars) || ES_MAX_PREVIEW }),\n    raw_response_preview: sanitizeErrorPreview(rec.raw_response_preview, { max_chars: (opts && opts.max_chars) || ES_MAX_PREVIEW }),\n    agent_request_id: esStr(rec.agent_request_id),\n    source_run_id: esStr(rec.source_run_id),\n    run_id: esStr(rec.run_id),\n    created_at: esStr(rec.created_at)\n  };\n}\n// --- end embedded error_sanitizer ---\n\n// MERGE LLM ENRICHMENT INTO DETERMINISTIC ROW (DEC-085).\n// Deterministic classification (det) is the source of truth for route/recommended_action/entity/contact safety.\n// Claude enrichment only improves descriptive fields + scores (within deterministic floors).\n// parse_method: primary_json | repaired_json | deterministic_fallback_after_llm_fail | technical_error.\nconst data=$json;\nconst src=$('Prepare Record').first().json;\nconst det=src.det||{};\n// --- Stage C Closure Patch 3: canonical source lineage (same chain WF16 uses to key source_health) ---\nfunction __lineage(pm, route){\n  var srid=str(src.source_run_id)||str(src.run_id)||str(src.agent_request_id);\n  var __cfg=$('Set Analyzer Config').first().json||{};\n  var dmode=(str(src.data_mode)||str(__cfg.data_mode)||'live').toLowerCase();\n  var p=String(pm||'').toLowerCase(), r=String(route||'').toLowerCase();\n  var pending=(p==='deterministic_uncertain_no_llm');\n  var review=pending?'pending':((str(src.review_status)||'confirmed').toLowerCase());\n  var wfQ=(r==='technical_errors'||r==='skipped_log'||p.indexOf('irrelevant')>=0||p==='technical_error')?'quarantined':((pending||p==='deterministic_fallback_after_llm_fail')?'degraded':'healthy');\n  var srcQ=String(src.quality_status||'').toLowerCase();\n  var rank={healthy:0,degraded:1,quarantined:2};\n  var quality=(rank[srcQ]!==undefined&&rank[srcQ]>rank[wfQ])?srcQ:wfQ;\n  var reportFalse=(src.report_eligible===false||String(src.report_eligible).toLowerCase()==='false');\n  var reportElig=!(quality==='quarantined'||pending||reportFalse);\n  return {source_run_id:srid,data_mode:dmode,quality_status:quality,review_status:review,report_eligible:reportElig,quality_flags:str(src.quality_flags)};\n}\nfunction str(v){return v==null?'':String(v).trim();}\nfunction clamp(v){const n=parseInt(v);return isNaN(n)?1:Math.min(100,Math.max(1,n));}\nfunction truncate(s,n){return (s==null?'':String(s)).substring(0,n);}\nfunction cleanContact(raw){\n  const s=String(raw||'').trim();\n  if(!s)return '';\n  if(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/.test(s))return s;\n  if(/t\\.me\\/|wa\\.me\\//i.test(s))return s;\n  if(/(^|[^A-Za-z0-9_])@[A-Za-z0-9_]{4,}/.test(s))return s;\n  const digits=s.replace(/\\D/g,'');\n  if(digits.length>=10&&digits.length<=11)return s;\n  return '';\n}\nfunction moscowIsoNow(){const m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nconst now=moscowIsoNow();\nconst run_id=src.run_id||'';\nconst batch_index=src.batch_index||0;\nconst validRoutes=['results','review_queue','monitor_queue','content_queue','skipped_log'];\nconst allowedSvc=['secured_auto_loan','secured_real_estate_loan','pts_loan','refinancing','mortgage_adjacent','generic_lending','credit_broker','consumer_credit','unknown'];\n\n// parse_method + enrichment object\nconst enr=(data.parse_ok===true && data.enrichment && typeof data.enrichment==='object')?data.enrichment:null;\nlet parse_method = enr ? ((data.parse_method==='repaired_json')?'repaired_json':'primary_json') : 'deterministic_fallback_after_llm_fail';\n\n// route is deterministic. If det has no valid route -> technical_errors (should not happen for the fixtures).\nlet route=det.route||'';\nif(validRoutes.indexOf(route)===-1){\n  return [{ json:{\n    created_at:now, source_type:str(src.source_type), platform:str(src.platform), source_url:str(src.source_url),\n    parsed_at:str(src.parsed_at)||now, published_at:str(src.published_at), freshness_status:'unknown',\n    entity_type:'irrelevant', company_name:'', profile_name:str(src.profile_name), profile_url:str(src.profile_url),\n    region:str(src.region_hint), service_type:'unknown', offer_text:'', terms:'', contact_public:'',\n    text_context:truncate(src.analyzer_text||src.text_context||'',3500), detected_need:'',\n    competitor_strength:1, lead_signal_score:1, content_idea_score:1, quality_score:1,\n    reason:'\u0414\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u0434\u0430\u043b\u0430 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430; \u0437\u0430\u043f\u0438\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 technical_errors \u0434\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438.',\n    recommended_action:'ignore', status:'analyzed', processing_status:'technical_error',\n    parse_method:'technical_error', parse_error: sanitizeErrorPreview(data.parse_error||'no_valid_det_route'),\n    raw_response_preview: sanitizeErrorPreview(data.raw_response_preview||'non_json_non_text_or_thinking_response'),\n    route:'technical_errors', needs_manual_review:true, repair_used:true, repair_status:'failed', run_id:run_id, batch_index:batch_index,\n  source_run_id:__linTech.source_run_id, data_mode:__linTech.data_mode, quality_status:__linTech.quality_status, review_status:__linTech.review_status, report_eligible:__linTech.report_eligible, quality_flags:__linTech.quality_flags\n  }}];\n}\n\nlet entity=det.entity_type||'content_idea';\nlet action=det.recommended_action||'investigate';\nconst contact=cleanContact(det.contact||'');\nconst hasContact=contact!=='';\n\n// deterministic safety floor (belt and suspenders): no results/contact without usable contact.\nif(route==='results' && !hasContact){ route='review_queue'; action='investigate'; }\nif(route==='results' && action!=='contact'){ action='contact'; }\n\n// base (deterministic) descriptive + scores\nlet company_name=str(det.company_name);\nlet profile_name=str(src.profile_name);\nlet region=str(det.region)||str(src.region_hint);\nlet service_type=(allowedSvc.indexOf(String(det.service_type||'').toLowerCase())>=0)?String(det.service_type).toLowerCase():'unknown';\nlet offer_text=truncate(det.offer_text||'',300);\nlet terms='';\nlet detected_need=truncate(det.detected_need||'',300);\nlet reason='';\nlet leadScore=clamp(det.lead_signal_score||1);\nlet compScore=clamp(det.competitor_strength||1);\nlet contentScore=clamp(det.content_idea_score||1);\nlet qualScore=clamp(det.quality_score||1);\n\nif(enr){\n  if(str(enr.company_name)) company_name=truncate(enr.company_name,160);\n  if(str(enr.profile_name)) profile_name=truncate(enr.profile_name,160);\n  if(str(enr.region)) region=truncate(enr.region,120);\n  const es=String(enr.service_type||'').toLowerCase();\n  if(es && allowedSvc.indexOf(es)>=0) service_type=es;\n  if(str(enr.offer_text)) offer_text=truncate(enr.offer_text,300);\n  if(str(enr.terms)) terms=truncate(enr.terms,220);\n  if(str(enr.detected_need)) detected_need=truncate(enr.detected_need,300);\n  if(str(enr.reason)) reason=truncate(enr.reason,600);\n  // scores: scale a 1-10 response to 1-100, then floor at the deterministic value.\n  const rs=[enr.lead_signal_score,enr.competitor_strength,enr.content_idea_score,enr.quality_score].map(function(v){var n=parseInt(v);return isNaN(n)?0:n;});\n  const maxRaw=Math.max.apply(null,rs);\n  const scale=(maxRaw>0&&maxRaw<=10)?10:1;\n  if(rs[0]>0) leadScore=Math.max(leadScore,clamp(rs[0]*scale));\n  if(rs[1]>0) compScore=Math.max(compScore,clamp(rs[1]*scale));\n  if(rs[2]>0) contentScore=Math.max(contentScore,clamp(rs[2]*scale));\n  if(rs[3]>0) qualScore=Math.max(qualScore,clamp(rs[3]*scale));\n}\n// RU-NARRATIVE guard (MVP-F): the persisted narrative must be Russian. A parse-ok enrichment whose reason\n// contains no Cyrillic is treated as an invalid narrative and replaced by the deterministic Russian reason.\nif(reason && !/[\u0430-\u044f\u0451]/i.test(reason)){ reason=''; }\nif(!reason){\n  reason='\u0414\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e \u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430\u043c \u043f\u0440\u0438\u0451\u043c\u0430 raw_market_records: \u043c\u0430\u0440\u0448\u0440\u0443\u0442 '+route+', \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 '+action+'.'+(enr?'':' Claude \u043d\u0435 \u0432\u0435\u0440\u043d\u0443\u043b \u0432\u0430\u043b\u0438\u0434\u043d\u044b\u0439 JSON \u2014 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0430 \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430 (fallback).');\n}\n// Reason safety sanitizer (DEC-087). Deterministic guarantee regardless of what Claude returned:\n// (a) NO direct-outreach phrasing when there is no usable public contact;\n// (b) NO unsupported demand/volume/conversion trend claims that are not present in ORIGINAL_RECORD.\nconst origLow=String(src.analyzer_text||src.text_context||'').toLowerCase();\nif(!hasContact && /(\u043e\u0431\u0440\u0430\u0442[\u0430\u0438]\\w*\\s+\u043d\u0430\u043f\u0440\u044f\u043c\u0443|\u043d\u0430\u043f\u0440\u044f\u043c\u0443\\w*|\u043d\u0430\u043f\u0438\u0448\\w*|\u043d\u0430\u043f\u0438\u0441\u0430\\w*|\u043f\u043e\u0437\u0432\u043e\u043d\\w*|\u0441\u0432\u044f\u0437\u0430\u0442\\w*|\u0441\u0432\u044f\u0436\u0438\u0442\\w*)/i.test(reason)){\n  reason='\u0420\u044b\u043d\u043e\u0447\u043d\u044b\u0439 \u043b\u0438\u0434-\u043f\u0430\u0442\u0442\u0435\u0440\u043d. \u041f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0430 \u043d\u0435\u0442, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0443\u0447\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0432\u0435\u0442\u043a\u0438/\u0430\u0432\u0442\u043e\u0440\u0430; \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0435\u0432 \u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u0446\u0438\u0438 \u0438 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430.';\n}\nconst trendRe=/(\u0441\u043f\u0440\u043e\u0441[\u0430-\u044f\u0451]*\\s+\u0440\u0430\u0441\u0442[\u0430-\u044f\u0451]*|\u0430\u043a\u0442\u0438\u0432\u043d\u043e\\s+\u0437\u0430\u0434\u0430\u044e[\u0430-\u044f\u0451]*|\u043c\u043d\u043e\u0433\u043e\\s+\u043b\u0438\u0434\u043e\u0432|\u0432\u044b\u0441\u043e\u043a[\u0430-\u044f\u0451]*\\s+\u043a\u043e\u043d\u0432\u0435\u0440\u0441[\u0430-\u044f\u0451]*)/i;\nif(trendRe.test(reason) && !trendRe.test(origLow)){\n  reason=reason.replace(new RegExp(trendRe.source,'gi'),'\u0435\u0441\u0442\u044c \u0440\u044b\u043d\u043e\u0447\u043d\u044b\u0439 \u043f\u0430\u0442\u0442\u0435\u0440\u043d').replace(/\\s{2,}/g,' ').trim();\n}\n\n// Source-candidate / social-channel safety assertion (DEC-088 Task D): a public social source can never become\n// a lead/competitor/outreach row, regardless of LLM output. Mirrors the specialized enrichment detection.\nconst sa_tpt=String(src.touchpoint_type||'').toLowerCase();\nconst sa_rth=String(src.record_type_hint||'').toLowerCase();\nconst sa_plat=String(src.platform||'').toLowerCase();\nconst sa_stype=String(src.source_type||'').toLowerCase();\nconst isSocialSource=(sa_rth==='market_signal'&&sa_tpt==='source_candidate')||(sa_stype==='social_channel')||(sa_plat==='telegram'&&String(det.entity_type||'')==='content_idea'&&!hasContact);\nif(isSocialSource){\n  if(det.route!=='review_queue'){ route='content_queue'; }\n  entity='content_idea';\n  if(action!=='create_content'&&action!=='investigate'){ action='create_content'; }\n  const directRequest=(sa_rth==='question_objection')||/(^|[^\u0430-\u044f\u0451])(\u043d\u0443\u0436\u0435\u043d|\u043d\u0443\u0436\u043d\u0430|\u043d\u0443\u0436\u043d\u043e|\u0438\u0449\u0443|\u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0438\u0442\u0435|\u043f\u043e\u043c\u043e\u0433\u0438\u0442\u0435|\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439\u0442\u0435|\u043a\u0442\u043e \u0434\u0430[\u0451\u0435]\u0442|\u043e\u0434\u043e\u0431\u0440\u044f\u0442 \u043b\u0438|\u0433\u0434\u0435 \u0432\u0437\u044f\u0442\u044c)([^\u0430-\u044f\u0451]|$)/i.test(origLow);\n  if(!directRequest){ leadScore=1; }\n  if(sa_rth!=='competitor_activity'){ compScore=1; }\n}\nlet rawPrev=truncate(data.raw_response_preview||'',500);\nif(parse_method==='deterministic_fallback_after_llm_fail' && (!rawPrev || rawPrev.indexOf('{')===-1)) rawPrev='non_json_non_text_or_thinking_response';\nif(parse_method==='primary_json') rawPrev=truncate(rawPrev,200);\n\nconst detFb=(parse_method==='deterministic_fallback_after_llm_fail');\nconst needs_manual_review=detFb?true:(det.needs_manual_review===true);\nconst repair_used=(parse_method==='repaired_json')||detFb;\nconst repair_status=(parse_method==='repaired_json')?'success':(detFb?'failed_fallback':'');\n\nconst __linMain=__lineage(parse_method, route);\nreturn [{ json:{\n  created_at: str(src.parsed_at)||now,\n  source_type: str(src.source_type),\n  platform: str(src.platform),\n  source_url: str(src.source_url),\n  parsed_at: str(src.parsed_at)||now,\n  published_at: str(src.published_at),\n  freshness_status: 'unknown',\n  entity_type: entity,\n  company_name: company_name,\n  profile_name: profile_name,\n  profile_url: str(src.profile_url),\n  region: region,\n  service_type: service_type,\n  offer_text: offer_text,\n  terms: terms,\n  contact_public: contact,\n  text_context: truncate(src.analyzer_text||src.text_context||'',3500),\n  detected_need: detected_need,\n  competitor_strength: compScore,\n  lead_signal_score: leadScore,\n  content_idea_score: contentScore,\n  quality_score: qualScore,\n  reason: reason,\n  recommended_action: action,\n  status: det.status||'analyzed',\n  processing_status: 'parsed_success',\n  parse_method: parse_method,\n  parse_error: sanitizeErrorPreview(data.parse_error||''),\n  raw_response_preview: sanitizeErrorPreview(rawPrev),\n  route: route,\n  needs_manual_review: needs_manual_review,\n  repair_used: repair_used,\n  repair_status: repair_status,\n  run_id: run_id,\n  batch_index: batch_index,\n  source_run_id:__linMain.source_run_id, data_mode:__linMain.data_mode, quality_status:__linMain.quality_status, review_status:__linMain.review_status, report_eligible:__linMain.report_eligible, quality_flags:__linMain.quality_flags\n}}];"
      }
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000ap",
      "name": "Append to Dynamic Route Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        2980,
        440
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $json.route }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueRegularOutput",
      "alwaysOutputData": true
    },
    {
      "id": "a8000000-0000-0000-0000-0000000000fs",
      "name": "Final Summary Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        180
      ],
      "parameters": {
        "jsCode": "function str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Analyzer Config').first().json;\nlet selected_count=0;\ntry{selected_count=$('Filter & Select Records').all().length;}catch(e){selected_count=0;}\n// Loop accounting fix (DEC-134): SplitInBatches (batch size 1) runs the in-loop nodes once per item, so\n// $('Build Deterministic Row').all() / $('Merge ...').all() AFTER the loop return only the LAST iteration ->\n// total_processed/deterministic_rows/route_counts were undercounted (operator saw 1 instead of 8). Aggregate\n// instead over the rows emitted on the loop's \"done\" output ($input), which carries EVERY processed row that\n// looped back through \"Loop Over Items\". Core routing logic is unchanged.\nlet processed=[];\ntry{processed=$input.all().map(i=>i.json).filter(function(r){return r&&(str(r.parse_method)!==''||str(r.route)!=='');});}catch(e){processed=[];}\nif(processed.length===0){\n  // legacy fallback (keeps a coherent summary if the done branch yields no processed rows)\n  let analyzed=[];let deterministic=[];\n  try{analyzed=$('Merge LLM Enrichment With Deterministic Row').all().map(i=>i.json);}catch(e){analyzed=[];}\n  try{deterministic=$('Build Deterministic Row').all().map(i=>i.json);}catch(e){deterministic=[];}\n  processed=analyzed.concat(deterministic);\n}\nconst claudeMethods=['primary_json','repaired_json','deterministic_fallback_after_llm_fail','technical_error'];\nconst detMethods=['deterministic_pre_route','deterministic_irrelevant_skip','deterministic_uncertain_no_llm'];\nconst route_counts={};const entity_counts={};const parse_method_counts={};\nlet repair_used=0,tech=0,detfb=0,preroute=0,irrelskip=0,uncblocked=0,claude_path=0,det_rows=0;\nfor(const r of processed){\n  const rt=str(r.route)||'unknown';route_counts[rt]=(route_counts[rt]||0)+1;\n  const en=str(r.entity_type)||'unknown';entity_counts[en]=(entity_counts[en]||0)+1;\n  const pm=str(r.parse_method)||'unknown';parse_method_counts[pm]=(parse_method_counts[pm]||0)+1;\n  if(r.repair_used===true)repair_used++;\n  if(rt==='technical_errors')tech++;\n  if(pm==='deterministic_fallback_after_llm_fail')detfb++;\n  if(pm==='deterministic_pre_route')preroute++;\n  if(pm==='deterministic_irrelevant_skip')irrelskip++;\n  if(pm==='deterministic_uncertain_no_llm')uncblocked++;\n  if(claudeMethods.indexOf(pm)>=0)claude_path++; else if(detMethods.indexOf(pm)>=0)det_rows++;\n}\nconst total_processed=processed.length;\nlet zero_record_diagnostics='';\nif(selected_count===0){\n  zero_record_diagnostics='0 records selected. Check: (1) agent_request_id_filter \u2014 duplicate-run request ids yield 0 by design (their rows have approval_status=duplicate, default analyze_statuses=[approved,new]); use the FIRST connector run id; (2) platform_filter/source_type_filter typos; (3) rows must have dedup_status=unique; (4) test_mode=false excludes status new and irrelevant; (5) max_records>0.';\n}\n// accounting coherence (DEC-134): processed rows split exactly into claude-path + deterministic, and never\n// exceed the selected count.\nconst accounting_ok=(total_processed===(claude_path+det_rows))&&(selected_count===0||total_processed<=selected_count);\nreturn [{ json: {\n  workflow:'08 - Touchpoint Analyzer',\n  run_id:cfg.run_id||'',\n  analysis_mode:cfg.analysis_mode||'deterministic_first',\n  llm_enabled:cfg.llm_enabled===true,\n  llm_enrichment:cfg.llm_enrichment===true,\n  test_mode:cfg.test_mode===true,\n  llm_enrichment_test_mode:cfg.llm_enrichment_test_mode===true,\n  llm_test_batch_indexes:cfg.llm_test_batch_indexes||[],\n  selected_count:selected_count,\n  total_processed:total_processed,\n  claude_path_rows:claude_path,\n  deterministic_rows:det_rows,\n  deterministic_pre_route:preroute,\n  deterministic_irrelevant_skip:irrelskip,\n  deterministic_uncertain_no_llm:uncblocked,\n  deterministic_fallback_after_llm_fail:detfb,\n  claude_calls:(cfg.llm_enabled===true)?claude_path:0,\n  estimated_analysis_cost_usd:(cfg.llm_enabled===true&&claude_path>0)?null:0,\n  zero_record_diagnostics:zero_record_diagnostics,\n  processed_accounting_ok:accounting_ok,\n  route_counts:route_counts,\n  entity_counts:entity_counts,\n  parse_method_counts:parse_method_counts,\n  repair_used_count:repair_used,\n  technical_errors_count:tech,\n  technical_error_count:tech,\n  next_action:(selected_count===0?zero_record_diagnostics:'Review the 6 business tabs and record outcomes in docs/STAGE_3_2_TEST_RESULTS.md. deterministic_first + llm_enrichment=false => Claude calls=0, repair_used=0, technical_errors=0 for the 12-record fixture.')\n}}];"
      }
    },
    {
      "id": "aa080000-0000-0000-0000-00000000diag",
      "name": "Handoff Diagnostics Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        860,
        -320
      ],
      "parameters": {
        "height": 300,
        "width": 420,
        "content": "## \u0414\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0430: \u00abFilter & Select Records \u0432\u0435\u0440\u043d\u0443\u043b 0 \u0437\u0430\u043f\u0438\u0441\u0435\u0439\u00bb\n\n\u0415\u0441\u043b\u0438 \u043f\u043e\u0441\u043b\u0435 handoff \u0438\u0437 \u043a\u043e\u043d\u043d\u0435\u043a\u0442\u043e\u0440\u0430 (WF09/WF11) \u0432\u044b\u0431\u043e\u0440\u043a\u0430 \u043f\u0443\u0441\u0442\u0430 \u2014 \u044d\u0442\u043e \u043f\u043e\u0447\u0442\u0438 \u0432\u0441\u0435\u0433\u0434\u0430 \u041d\u0415 \u0431\u0430\u0433. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e \u043f\u043e\u0440\u044f\u0434\u043a\u0443:\n1. `agent_request_id_filter` \u2014 id \u0438\u043c\u0435\u043d\u043d\u043e \u041f\u0415\u0420\u0412\u041e\u0413\u041e \u043f\u0440\u043e\u0433\u043e\u043d\u0430 \u043a\u043e\u043d\u043d\u0435\u043a\u0442\u043e\u0440\u0430. \u041f\u043e\u0432\u0442\u043e\u0440\u043d\u044b\u0435 (duplicate) \u043f\u0440\u043e\u0433\u043e\u043d\u044b \u043f\u0438\u0448\u0443\u0442 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441 `approval_status=duplicate`, \u0430 \u0434\u0435\u0444\u043e\u043b\u0442 `analyze_statuses=['approved','new']` \u0438\u0445 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u0435\u0442.\n2. `approval_status` \u0441\u0442\u0440\u043e\u043a \u0432 `raw_market_records` vs `analyze_statuses`.\n3. `platform_filter` / `source_type_filter` \u2014 \u043d\u0435 \u043e\u0441\u0442\u0430\u043b\u0438\u0441\u044c \u043b\u0438 \u043e\u0442 \u043f\u0440\u043e\u0448\u043b\u043e\u0433\u043e \u0442\u0435\u0441\u0442\u0430.\n4. `max_records` > 0.\n\n\u0410\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c duplicate-\u0441\u0442\u0440\u043e\u043a\u0438 \u043c\u043e\u0436\u043d\u043e, \u044f\u0432\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0432 `duplicate` \u0432 `analyze_statuses`, \u043d\u043e \u0434\u043b\u044f \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e handoff \u044d\u0442\u043e \u041d\u0415 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f (\u0434\u0443\u0431\u043b\u0438\u043a\u0430\u0442\u044b \u2014 \u0430\u0443\u0434\u0438\u0442, \u043d\u0435 \u043d\u043e\u0432\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435)."
      }
    },
    {
      "id": "wf08-read-analysisruns",
      "name": "Read analysis_runs",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        660,
        300
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "analysis_runs",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "wf08-build-analysisruns",
      "name": "Build analysis_runs Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        820
      ],
      "parameters": {
        "jsCode": "// WF08 analysis_runs ledger writer (Stage 3 closure, DEC-148). One row per SELECTED source record = the\n// exactly-once idempotency key the next run reads back. Runs once per WF08 execution. No external calls.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nconst cfg=$('Set Analyzer Config').first().json||{};\nconst sel=$('Filter & Select Records').all().map(function(i){return i.json;});\nconst now=moscowIsoNow();\nreturn sel.map(function(r){ var sid=str(r.source_run_id), rid=str(r.source_record_id)||str(r.record_id);\n  return { json: {\n    analysis_run_id:str(r.analysis_run_id)||str(cfg.analysis_run_id),\n    analysis_idempotency_key:str(r.analysis_idempotency_key)||(sid+'::'+rid),\n    source_run_id:sid, source_record_id:rid, agent_request_id:str(r.agent_request_id),\n    platform:str(r.platform), source_type:str(r.source_type), data_mode:str(r.data_mode)||'live',\n    analysis_status:'analyzed', analyzed_at:now, analysis_mode:str(cfg.analysis_mode), llm_used:false,\n    notes:'wf08 analysis ledger (exactly-once)'\n  }};\n});"
      }
    },
    {
      "id": "wf08-append-analysisruns",
      "name": "Append analysis_runs",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1320,
        820
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "analysis_runs",
          "mode": "name"
        },
        "options": {},
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        }
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "inputSource": "workflowInputs",
        "workflowInputs": {
          "values": [
            {
              "name": "agent_request_id",
              "type": "string"
            },
            {
              "name": "source_run_id",
              "type": "string"
            },
            {
              "name": "workflow_run_id",
              "type": "string"
            },
            {
              "name": "data_mode",
              "type": "string"
            },
            {
              "name": "force_reprocess",
              "type": "string"
            },
            {
              "name": "platform_filter",
              "type": "string"
            },
            {
              "name": "source_type_filter",
              "type": "string"
            },
            {
              "name": "llm_enabled",
              "type": "string"
            },
            {
              "name": "llm_approval_token",
              "type": "string"
            },
            {
              "name": "llm_budget_usd",
              "type": "string"
            },
            {
              "name": "request_cancelled",
              "type": "string"
            },
            {
              "name": "max_records",
              "type": "string"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        120,
        520
      ],
      "id": "wf08-subtrig",
      "name": "When Called by Agent"
    }
  ],
  "connections": {
    "Manual Start": {
      "main": [
        [
          {
            "node": "Set Analyzer Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Analyzer Config": {
      "main": [
        [
          {
            "node": "Read analysis_runs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read raw_market_records": {
      "main": [
        [
          {
            "node": "Filter & Select Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter & Select Records": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build analysis_runs Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Final Summary Output",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Record": {
      "main": [
        [
          {
            "node": "IF Call Claude?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Call Claude?": {
      "main": [
        [
          {
            "node": "Build Primary Claude Request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Deterministic Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Deterministic Row": {
      "main": [
        [
          {
            "node": "Append to Dynamic Route Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Primary Claude Request": {
      "main": [
        [
          {
            "node": "Claude Primary API Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Primary API Request": {
      "main": [
        [
          {
            "node": "Parse Primary JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Primary JSON": {
      "main": [
        [
          {
            "node": "IF Primary Parse OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Primary Parse OK?": {
      "main": [
        [
          {
            "node": "Merge LLM Enrichment With Deterministic Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Repair Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Repair Request": {
      "main": [
        [
          {
            "node": "Claude Repair API Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Repair API Request": {
      "main": [
        [
          {
            "node": "Parse Repaired JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Repaired JSON": {
      "main": [
        [
          {
            "node": "Merge LLM Enrichment With Deterministic Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to Dynamic Route Sheet": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge LLM Enrichment With Deterministic Row": {
      "main": [
        [
          {
            "node": "Append to Dynamic Route Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read analysis_runs": {
      "main": [
        [
          {
            "node": "Read raw_market_records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build analysis_runs Row": {
      "main": [
        [
          {
            "node": "Append analysis_runs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Called by Agent": {
      "main": [
        [
          {
            "node": "Set Analyzer Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a8000000-touchpoint-analyzer-v010-loop-summary-accounting-20260616"
}