{
  "name": "14 - Lead Scout Triage & Scoring (Deterministic, Stage 3.5)",
  "nodes": [
    {
      "parameters": {
        "content": "## WF14 \u2014 Lead Scout Triage & Scoring v0.3 (Stage 3.5, DEC-138)\n\n**\u041d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435:** \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u043b\u043e\u0439 Lead Scout. $0, \u0411\u0415\u0417 Claude, \u0411\u0415\u0417 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0432\u044b\u0437\u043e\u0432\u043e\u0432. \u0427\u0438\u0442\u0430\u0435\u0442 raw_market_records (audience-\u0441\u0442\u0440\u043e\u043a\u0438 \u2014 \u041f\u0415\u0420\u0412\u0418\u0427\u041d\u041e, \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 WF08) + review_queue (\u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435), \u0441\u043a\u043e\u0440\u0438\u0442 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b (0-100), \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0422\u041e\u041b\u042c\u041a\u041e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b-\u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430 \u0438 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0432\u0441\u0451 \u043d\u0430 \u0420\u0423\u0427\u041d\u0423\u042e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043e\u043c. \u041f\u0438\u0448\u0435\u0442 public_lead_signals (v0.3, 47 \u043a\u043e\u043b\u043e\u043d\u043e\u043a, docs/TABLE_SCHEMA.md \u00a7G) + agent_requests.\n\n**\u0421\u043a\u043e\u0440\u0438\u043d\u0433 (\u0434\u0435\u0442\u0435\u0440\u043c.):** intent(25)+urgency(15)+pain(20)+niche(15)+contact(8)+region(7)+freshness(10) - \u0448\u0442\u0440\u0430\u0444\u044b. \u041f\u043e\u043b\u043e\u0441\u044b: >=75 high / 50-74 medium / 25-49 low / <25 ignore (\u043d\u0435 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u043f\u0440\u0438 min_lead_score=25). \u041f\u043e\u043b\u044f: lead_score, score_band, score_reasons, review_priority, recommended_action.\n\n**pain_type:** bank_refusal \u00b7 bad_credit_history \u00b7 overdue_debt \u00b7 refinancing_need \u00b7 mortgage_need \u00b7 business_finance_need \u00b7 pledge_auto_pts \u00b7 broker_price_question \u00b7 fraud_fear \u00b7 prepayment_fear \u00b7 document_problem \u00b7 unknown.\n**intent_type:** question \u00b7 objection \u00b7 complaint \u00b7 buying_intent \u00b7 content_signal.\n\n**\u0416\u0451\u0441\u0442\u043a\u0438\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 (CONTACT_AND_OUTREACH_POLICY):** \u0442\u043e\u043b\u044c\u043a\u043e \u041f\u0423\u0411\u041b\u0418\u0427\u041d\u042b\u0415 \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430; \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c/\u043a\u043e\u043d\u0442\u0430\u043a\u0442 = evidence, \u041d\u0415 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u0430\u0443\u0442\u0440\u0438\u0447; outreach_allowed=false \u0412\u0421\u0415\u0413\u0414\u0410; recommended_action \u2208 {manual_review, content_idea, monitor, ignore}; \u043a\u043e\u043d\u0442\u0430\u043a\u0442 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0432 \u0442\u0435\u043a\u0441\u0442\u0435 + \u0435\u0441\u0442\u044c contact_source_url, \u0438\u043d\u0430\u0447\u0435 \u0431\u043b\u0430\u043d\u043a + privacy_flag; \u043d\u0435\u0442 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0445 \u0447\u0430\u0442\u043e\u0432 / \u0441\u043a\u0440\u044b\u0442\u044b\u0445 / \u0432\u044b\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432 / \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0438 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432. review_status: new \u2192 needs_review \u2192 accepted/rejected/duplicate/stale. \u0414\u0435\u0434\u0443\u043f: lead_signal_id (\u0445\u044d\u0448) + comment_url/post_url+texthash/profile_url+texthash/platform+texthash. \u041a\u0432\u043e\u0442\u0430-safe (DEC-131): \u043a\u0430\u0436\u0434\u0430\u044f \u0432\u043a\u043b\u0430\u0434\u043a\u0430 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044f 1 \u0440\u0430\u0437, \u043f\u0443\u043b \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d, append \u226425. active=false; \u0440\u0443\u0447\u043d\u043e\u0439 \u0437\u0430\u043f\u0443\u0441\u043a; MSK +03:00.",
        "height": 640,
        "width": 660
      },
      "id": "wf14-note-1",
      "name": "Overview Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -480,
        -360
      ]
    },
    {
      "parameters": {
        "content": "## Setup & Test (v0.3 \u2014 Stage 3.5)\n\n1. \u0412\u043a\u043b\u0430\u0434\u043a\u0430 public_lead_signals = 47 \u043a\u043e\u043b\u043e\u043d\u043e\u043a v0.3 (docs/TABLE_SCHEMA.md \u00a7G). \u041c\u0438\u0433\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0440\u0443\u044e 28-\u043a\u043e\u043b\u043e\u043d\u043e\u0447\u043d\u0443\u044e \u0432\u043a\u043b\u0430\u0434\u043a\u0443 \u043f\u043e docs/GOOGLE_SHEETS_VALIDATION_PLAN. \u041d\u0415 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c.\n2. \u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c SPREADSHEET_ID \u0438 \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b \u043d\u0430 5 Google Sheets \u043d\u043e\u0434\u0430\u0445 (3 read + 2 append).\n3. \u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u2014 \u041d\u0415 \u043f\u043e \u043a\u0430\u0436\u0434\u043e\u0439 \u043d\u043e\u0434\u0435. \u041f\u043e\u043b\u043d\u0430\u044f \u043f\u0440\u0438\u0451\u043c\u043a\u0430 Stage 3.5 = \u043f\u0430\u043a\u0435\u0442 Stage C (\u0441\u043c. docs/STAGE_C_ACCEPTANCE_PACK.md). \u0411\u044b\u0441\u0442\u0440\u044b\u0439 smoke ($0): \u043f\u043e\u0441\u043b\u0435 WF13 fixture \u2192 WF14 Execute once \u2192 public_lead_signals: high-intent \u0441\u0442\u0440\u043e\u043a\u0438 (after_refusal/buying_intent) \u0441 lead_score, score_band, review_priority; \u043a\u043e\u043d\u0442\u0430\u043a\u0442-fixture \u0441 \u0441\u0438\u043d\u0442\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u043e\u043c \u2192 public_phone \u043a\u0430\u043a \u0442\u0435\u043a\u0441\u0442 (\u0431\u0435\u0437 #ERROR!) + contact_use_policy=manual_review; \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0439/\u043d\u043e\u0432\u043e\u0441\u0442\u043d\u043e\u0439 fixture \u041d\u0415 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u043b\u0438\u0434\u043e\u043c; outreach_allowed=false \u0432\u043e \u0432\u0441\u0435\u0445 \u0441\u0442\u0440\u043e\u043a\u0430\u0445; Final Summary self_test_passed=true.\n4. \u041f\u043e\u0432\u0442\u043e\u0440: signals_written=0, duplicates_skipped>0, status=completed_no_data.\n5. \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c self-test: read_once_ok / append_cap_ok / dedup_ok / policy_ok = true.",
        "height": 640,
        "width": 620
      },
      "id": "wf14-note-2",
      "name": "Setup and Test Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        220,
        -360
      ]
    },
    {
      "parameters": {},
      "id": "wf14-start",
      "name": "Manual Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -420,
        560
      ]
    },
    {
      "parameters": {
        "jsCode": "// WF14 v0.3 \u2014 Lead Scout Triage & Scoring config (Stage 3.5, DEC-138). Deterministic, $0, NO Claude, NO external calls.\n// Central lead engine: reads raw_market_records audience rows (PRIMARY) + review_queue (enrichment), scores public\n// lead signals, extracts ONLY public contact evidence, routes everything to MANUAL review. Quota-safe (DEC-131):\n// each tab read exactly once, bounded candidate pool, capped batched append. Public evidence only; a public\n// profile/contact is EVIDENCE, never outreach permission; outreach_allowed=false ALWAYS (CONTACT_AND_OUTREACH_POLICY).\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction moscowStamp(){var z=function(n){return String(n).padStart(2,'0');};var m=new Date(Date.now()+10800000);return m.getUTCFullYear()+z(m.getUTCMonth()+1)+z(m.getUTCDate())+'_'+z(m.getUTCHours())+z(m.getUTCMinutes())+z(m.getUTCSeconds());}\nconst stamp=moscowStamp();\nvar __cfg = {\n  // ---- schema / scope (quota safety; DEC-131)\n  schema_version:'public_lead_signals_v0.3',\n  time_window_days:30,\n  max_source_rows:120,            // working candidate pool cap (applied AFTER score/sort/filter)\n  max_signals_to_write:25,        // HARD ceiling on appended rows\n  platform_filter:'',             // '' = any; operator may set 'vk'\n  source_type_filter:'',          // '' = any\n  only_untriaged:true,\n  include_review_queue:true,      // clean-isolation: set false so old review_queue rows can't contaminate a WF13-run acceptance test\n  source_agent_request_id:'',     // '' = all sources; set to a WF13 run's agent_request_id to triage ONLY that source run (Defect G)\n  backfill_untriaged:true,        // allow older untriaged high-score rows when the fresh window is thin\n  // ---- scoring engine (deterministic; weights sum to 100 before penalties)\n  min_lead_score:25,              // below this -> not written (ignore band kept out of the sheet by default)\n  band_high:75,                   // >=75 high priority manual review\n  band_medium:50,                 // 50-74 medium\n  band_low:25,                    // 25-49 low (monitor / content idea)\n  score_weights:{ intent:25, urgency:15, pain:20, niche:15, contact:8, region:7, freshness:10 },\n  // ---- contact / outreach policy (CONTACT_AND_OUTREACH_POLICY: evidence, NOT permission)\n  contact_use_policy_default:'aggregate_only',\n  contact_use_policy_with_public_contact:'manual_review',\n  contact_use_policy_weak_evidence:'do_not_use',\n  outreach_allowed:false,         // ALWAYS false in Stage 3.5 \u2014 no auto-outreach, ever\n  allowed_recommended_actions:['manual_review','content_idea','monitor','ignore'],\n  forbidden_recommended_actions:['outreach','contact','dm','message','call','email','send'],\n  region_default:'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n  requested_by:'operator',\n  // ---- deterministic ids + MSK stamps\n  run_stamp:stamp,\n  run_id:'wf14_'+stamp,\n  agent_request_id:'wf14_req_'+stamp,\n  created_at:moscowIsoNow()\n};\n// D2 (VK-COMMENTS-001): request-scoping via the callable trigger. Manual runs (empty inputs) keep every default.\nvar __in={}; try{ __in=$('When Called by Agent').first().json||{}; }catch(e){ __in=(typeof $json!=='undefined'&&$json&&typeof $json==='object')?$json:{}; }\nfunction __num(v){var n=Number(v);return isNaN(n)?null:n;}\nfunction __bool(v){if(v===true||v===false)return v;var s=String(v==null?'':v).toLowerCase();if(s==='true')return true;if(s==='false')return false;return null;}\nif(String(__in.source_agent_request_id||'')!=='')__cfg.source_agent_request_id=String(__in.source_agent_request_id);\nif(String(__in.agent_request_id||'')!=='')__cfg.agent_request_id=String(__in.agent_request_id);\nif(String(__in.platform_filter||'')!=='')__cfg.platform_filter=String(__in.platform_filter);\nif(String(__in.source_type_filter||'')!=='')__cfg.source_type_filter=String(__in.source_type_filter);\nif(__num(__in.min_lead_score)!=null)__cfg.min_lead_score=__num(__in.min_lead_score);\nif(__bool(__in.include_review_queue)!=null)__cfg.include_review_queue=__bool(__in.include_review_queue);\nreturn [{ json: __cfg }];\n"
      },
      "id": "wf14-config",
      "name": "Set Triage Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -200,
        560
      ]
    },
    {
      "id": "wf14-read-review",
      "name": "Read review_queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        20,
        560
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "review_queue",
          "mode": "name"
        },
        "options": {}
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Collapse to a single control item so the NEXT Google Sheets read executes EXACTLY ONCE.\n// Prevents item explosion (a multi-item read feeding another read = read-per-item = quota error).\n// Sheet data stays available downstream via $('Read ...').all().\nreturn [{ json: $('Set Triage Config').first().json }];"
      },
      "id": "wf14-hold-a",
      "name": "Hold Config (before raw)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        200,
        560
      ]
    },
    {
      "id": "wf14-read-raw",
      "name": "Read raw_market_records",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        380,
        560
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "raw_market_records",
          "mode": "name"
        },
        "options": {}
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Collapse to a single control item so the NEXT Google Sheets read executes EXACTLY ONCE.\n// Prevents item explosion (a multi-item read feeding another read = read-per-item = quota error).\n// Sheet data stays available downstream via $('Read ...').all().\nreturn [{ json: $('Set Triage Config').first().json }];"
      },
      "id": "wf14-hold-b",
      "name": "Hold Config (before pls)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        560
      ]
    },
    {
      "id": "wf14-read-pls",
      "name": "Read public_lead_signals",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        740,
        560
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "public_lead_signals",
          "mode": "name"
        },
        "options": {}
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// WF14 v0.3 \u2014 LEAD SCOUT TRIAGE & SCORING ENGINE (deterministic, $0, no Claude; Stage 3.5, DEC-138).\n// Reads raw_market_records audience rows (PRIMARY) + review_queue (enrichment) ONCE each (upstream collapse nodes\n// guarantee single execution -> quota-safe, DEC-131). Classifies pain/intent, scores demand deterministically,\n// extracts ONLY public contact evidence, and routes everything to MANUAL review.\n// HARD RULES (CONTACT_AND_OUTREACH_POLICY): public evidence only; a public profile/contact is EVIDENCE, never\n// outreach permission; recommended_action constrained to {manual_review, content_idea, monitor, ignore} (outreach\n// impossible by construction); outreach_allowed=false ALWAYS; no hidden/inferred contacts; no member extraction.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\nfunction cut(s,n){s=str(s);return s.length>n?(s.slice(0,n-1)+'\u2026'):s;}\nfunction norm(s){return low(s).replace(/\\s+/g,' ').trim().slice(0,240);}\nfunction h32(s,seed){s=str(s);let h=seed>>>0;for(let i=0;i<s.length;i++){h=((h<<5)+h+s.charCodeAt(i))>>>0;}return h>>>0;}\nfunction hex(h){return ('0000000'+(h>>>0).toString(16)).slice(-8);}\nfunction sheetsSafe(v){v=str(v);return /^[+=]/.test(v)?(\"'\"+v):v;}\nfunction rows(name){try{return $(name).all().map(i=>i.json).filter(r=>r&&Object.keys(r).some(k=>str(r[k])!==''));}catch(e){return [];}}\n\nconst cfg=$('Set Triage Config').first().json;\nconst W=cfg.score_weights||{intent:25,urgency:15,pain:20,niche:15,contact:8,region:7,freshness:10};\nconst windowDays=Number(cfg.time_window_days)||30;\nconst cutoffMs=Date.now()-windowDays*86400000;\nconst maxSource=Math.max(1,Math.min(200,Number(cfg.max_source_rows)||120));\nconst maxWrite=Math.max(1,Math.min(25,Number(cfg.max_signals_to_write)||25)); // hard ceiling 25\nconst minScore=Number(cfg.min_lead_score)||0;\nconst bandHigh=Number(cfg.band_high)||75, bandMed=Number(cfg.band_medium)||50, bandLow=Number(cfg.band_low)||25;\nconst platformFilter=low(cfg.platform_filter);\nconst sourceTypeFilter=low(cfg.source_type_filter);\nconst backfill=cfg.backfill_untriaged!==false;\nconst ALLOWED=Array.isArray(cfg.allowed_recommended_actions)?cfg.allowed_recommended_actions:['manual_review','content_idea','monitor','ignore'];\nconst FORBIDDEN=['outreach','contact','dm','message','call','email','send'];\n\nconst includeReview=cfg.include_review_queue!==false;\nconst sourceReqId=low(cfg.source_agent_request_id);\nconst review=includeReview?rows('Read review_queue'):[];\nlet raw=rows('Read raw_market_records');\nif(sourceReqId){raw=raw.filter(r=>low(r.agent_request_id)===sourceReqId);}\nconst existing=rows('Read public_lead_signals');\n\n// ---- existing dedup (tolerate old 28-col names + new v0.3 names)\nfunction exPlatform(r){return low(r.source_platform||r.platform);}\nfunction exPost(r){return low(r.source_post_url||r.post_url);}\nfunction exCmt(r){return low(r.source_comment_url);}\nfunction exText(r){return norm(r.evidence_text||r.text_evidence);}\nfunction exProf(r){return low(r.source_public_profile_url||r.public_profile_url||r.profile_url);}\nconst existIds=new Set(existing.map(r=>low(r.lead_signal_id)).filter(Boolean));\nconst existKeyToId={};\nfunction addExKey(k,id){if(k&&!existKeyToId[k])existKeyToId[k]=id;}\nfor(const r of existing){\n  const id=low(r.lead_signal_id), p=exPlatform(r), post=exPost(r), cmt=exCmt(r), prof=exProf(r), th=hex(h32(exText(r),5381)), th2=hex(h32(exText(r),7919));\n  if(cmt)addExKey('c|'+p+'|'+cmt,id);\n  if(post)addExKey('p|'+p+'|'+post+'|'+th,id);\n  if(prof)addExKey('u|'+p+'|'+prof+'|'+th,id);\n  addExKey('h|'+p+'|'+th2,id);\n}\n\n// ---- vocabularies (credit_brokerage v0.3; canonical pain set per Stage 3.5 brief)\nconst INTENT_HIGH=['\u043d\u0443\u0436\u0435\u043d \u043a\u0440\u0435\u0434\u0438\u0442','\u043d\u0443\u0436\u0435\u043d \u0437\u0430\u0439\u043c','\u043d\u0443\u0436\u043d\u0430 \u0438\u043f\u043e\u0442\u0435\u043a','\u043d\u0443\u0436\u043d\u043e \u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441','\u043d\u0443\u0436\u043d\u044b \u0434\u0435\u043d\u044c\u0433\u0438','\u0438\u0449\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0430','\u0438\u0449\u0443 \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u043e\u0433\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430','\u043f\u043e\u043c\u043e\u0433\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c','\u043f\u043e\u043c\u043e\u0433\u0438\u0442\u0435 \u0432\u0437\u044f\u0442\u044c','\u0445\u043e\u0447\u0443 \u0432\u0437\u044f\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442','\u0433\u0434\u0435 \u0432\u0437\u044f\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','\u043a\u0442\u043e \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','\u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','\u043d\u0443\u0436\u0435\u043d \u0437\u0430\u043b\u043e\u0433\u043e\u0432\u044b\u0439','\u0434\u0435\u043d\u044c\u0433\u0438 \u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433','\u043d\u0443\u0436\u043d\u0430 \u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0430\u044f \u0433\u0430\u0440\u0430\u043d\u0442\u0438','\u043d\u0443\u0436\u0435\u043d \u0442\u0435\u043d\u0434\u0435\u0440\u043d\u044b\u0439'];\nconst INTENT_MED=['\u043f\u043e\u0447\u0435\u043c\u0443 \u043e\u0442\u043a\u0430\u0437','\u043a\u0430\u043a \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u0438','\u043a\u0430\u043a \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u0433\u0434\u0435 \u043b\u0443\u0447\u0448\u0435 \u0432\u0437\u044f\u0442\u044c','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e \u0441\u0442\u043e\u0438\u0442','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u043e\u0438\u0442','\u043a \u043a\u043e\u043c\u0443 \u043e\u0431\u0440\u0430\u0449\u0430\u0442\u044c\u0441\u044f','\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439\u0442\u0435','\u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0438\u0442\u0435','\u043a\u0442\u043e \u0431\u0440\u0430\u043b','\u043a\u0442\u043e \u043e\u0431\u0440\u0430\u0449\u0430\u043b\u0441\u044f','\u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c','\u043d\u0443\u0436\u0435\u043d \u0441\u043e\u0432\u0435\u0442'];\nconst QUESTION=['\u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0438\u0442\u0435','\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439\u0442\u0435','\u043a\u0442\u043e \u0431\u0440\u0430\u043b','\u043a\u0442\u043e \u043e\u0431\u0440\u0430\u0449\u0430\u043b\u0441\u044f','\u043a \u043a\u043e\u043c\u0443 \u043e\u0431\u0440\u0430\u0449\u0430\u0442\u044c\u0441\u044f','\u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c','\u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c','\u043d\u0443\u0436\u0435\u043d \u0441\u043e\u0432\u0435\u0442','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e \u0441\u0442\u043e\u0438\u0442','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u043e\u0438\u0442','?'];\nconst OBJECTION=['\u043d\u0435 \u0432\u0435\u0440\u044e','\u0431\u043e\u044e\u0441\u044c','\u0440\u0430\u0437\u0432\u043e\u0434','\u043e\u0431\u043c\u0430\u043d','\u043a\u0438\u0434\u0430\u043b','\u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442'];\nconst COMPLAINT=['\u0436\u0430\u043b\u043e\u0431','\u043e\u0431\u043c\u0430\u043d\u0443\u043b\u0438','\u043a\u0438\u043d\u0443\u043b\u0438','\u043d\u0435 \u0432\u0435\u0440\u043d\u0443\u043b\u0438','\u043e\u0442\u0432\u0440\u0430\u0442\u0438\u0442\u0435\u043b\u044c\u043d','\u0437\u0430\u0431\u0440\u0430\u043b\u0438 \u0434\u0435\u043d\u044c\u0433\u0438','\u0432\u0437\u044f\u043b\u0438 \u0434\u0435\u043d\u044c\u0433\u0438 \u0438'];\nconst URGENT_HIGH=['\u0441\u0440\u043e\u0447\u043d\u043e','\u0441\u0435\u0433\u043e\u0434\u043d\u044f','\u0434\u043e \u0437\u0430\u0432\u0442\u0440\u0430','\u0433\u043e\u0440\u0438\u0442','\u0433\u043e\u0440\u0438\u0442 \u0441\u0434\u0435\u043b\u043a\u0430','\u043e\u0447\u0435\u043d\u044c \u0441\u0440\u043e\u0447\u043d\u043e','\u0434\u0435\u043d\u044c\u0433\u0438 \u043d\u0443\u0436\u043d\u044b \u0441\u0440\u043e\u0447\u043d\u043e'];\nconst URGENT_MED=['\u0432 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0435','\u043f\u043b\u0430\u043d\u0438\u0440\u0443\u044e','\u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043d\u0435\u0434\u0435\u043b\u0435','\u0441\u043a\u043e\u0440\u043e \u043f\u043e\u043d\u0430\u0434\u043e\u0431','\u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043c\u0435\u0441\u044f\u0446'];\nconst PAIN=[\n ['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','bank_refusal'],['\u043e\u0442\u043a\u0430\u0437\u0430\u043b\u0438','bank_refusal'],['\u043e\u0442\u043a\u0430\u0437 \u0431\u0430\u043d\u043a','bank_refusal'],['\u0431\u0430\u043d\u043a \u043e\u0442\u043a\u0430\u0437','bank_refusal'],['\u043e\u0442\u043a\u0430\u0437\u043e\u0432 \u0431\u0430\u043d\u043a','bank_refusal'],['\u0431\u0430\u043d\u043a\u0438 \u043e\u0442\u043a\u0430\u0437','bank_refusal'],\n ['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d','bad_credit_history'],['\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','bad_credit_history'],['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0438','bad_credit_history'],['\u0438\u0441\u043f\u043e\u0440\u0447\u0435\u043d\u0430 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','bad_credit_history'],['\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438','bad_credit_history'],\n ['\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','overdue_debt'],['\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442','overdue_debt'],['\u0434\u043e\u043b\u0433 \u043f\u043e \u043a\u0440\u0435\u0434\u0438\u0442','overdue_debt'],\n ['\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440','refinancing_need'],['\u0441\u043d\u0438\u0437\u0438\u0442\u044c \u0441\u0442\u0430\u0432\u043a','refinancing_need'],['\u043f\u0435\u0440\u0435\u043a\u0440\u0435\u0434\u0438\u0442','refinancing_need'],\n ['\u0438\u043f\u043e\u0442\u0435\u043a','mortgage_need'],\n ['\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430','business_finance_need'],['\u0434\u043b\u044f \u0438\u043f','business_finance_need'],['\u0434\u043b\u044f \u043e\u043e\u043e','business_finance_need'],['\u043e\u0431\u043e\u0440\u043e\u0442\u043d','business_finance_need'],['\u0442\u0435\u043d\u0434\u0435\u0440\u043d','business_finance_need'],['\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a \u0433\u0430\u0440\u0430\u043d\u0442\u0438','business_finance_need'],\n ['\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433 \u043f\u0442\u0441','pledge_auto_pts'],['\u0437\u0430\u043b\u043e\u0433 \u0430\u0432\u0442\u043e','pledge_auto_pts'],['\u043f\u043e\u0434 \u043f\u0442\u0441','pledge_auto_pts'],['\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433 \u043d\u0435\u0434\u0432\u0438\u0436','pledge_auto_pts'],['\u0437\u0430\u043b\u043e\u0433 \u043d\u0435\u0434\u0432\u0438\u0436','pledge_auto_pts'],['\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433','pledge_auto_pts'],\n ['\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e \u0441\u0442\u043e\u0438\u0442','broker_price_question'],['\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u043e\u0438\u0442','broker_price_question'],['\u043a\u0430\u043a\u0430\u044f \u043a\u043e\u043c\u0438\u0441\u0441\u0438','broker_price_question'],['\u043f\u0440\u043e\u0446\u0435\u043d\u0442 \u0431\u0440\u043e\u043a\u0435\u0440','broker_price_question'],['\u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','broker_price_question'],\n ['\u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','fraud_fear'],['\u043e\u0431\u043c\u0430\u043d','fraud_fear'],['\u043a\u0438\u0434\u0430\u043b','fraud_fear'],['\u0440\u0430\u0437\u0432\u043e\u0434','fraud_fear'],\n ['\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442','prepayment_fear'],\n ['\u0441\u043f\u0440\u0430\u0432\u043a','document_problem'],['2-\u043d\u0434\u0444\u043b','document_problem'],['\u0431\u0435\u0437 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434','document_problem'],['\u043d\u0435\u0442 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442','document_problem']\n];\nconst SEVERE=['bank_refusal','overdue_debt','bad_credit_history','pledge_auto_pts'];\n// supplier/ad markers: a BROKER advertising is NOT a consumer lead -> excluded from the lead pool\nconst SUPPLIER=['\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440','\u0438\u043f\u043e\u0442\u0435\u0447\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440','\u043f\u043e\u043c\u043e\u0449\u044c \u0432 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u043a\u0440\u0435\u0434\u0438\u0442\u0430','\u043f\u043e\u043c\u043e\u0433\u0430\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c','\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435 \u043a\u0440\u0435\u0434\u0438\u0442\u0430','\u043f\u043e\u0434\u0431\u043e\u0440 \u0431\u0430\u043d\u043a\u0430','\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0437\u0430\u044f\u0432\u043a\u0443','\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443','\u043d\u0430\u0448\u0438 \u0443\u0441\u043b\u0443\u0433\u0438','\u0437\u0432\u043e\u043d\u0438\u0442\u0435','\u043f\u0438\u0448\u0438\u0442\u0435 \u0432 \u043b\u0438\u0447\u043a\u0443','\u043e\u043f\u043b\u0430\u0442\u0430 \u043f\u043e \u0444\u0430\u043a\u0442\u0443'];\n// strong niche service evidence (raises niche_fit)\nconst NICHE_STRONG=['\u043a\u0440\u0435\u0434\u0438\u0442','\u0437\u0430\u0439\u043c','\u0438\u043f\u043e\u0442\u0435\u043a','\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441','\u0437\u0430\u043b\u043e\u0433','\u0431\u0440\u043e\u043a\u0435\u0440','\u043e\u0442\u043a\u0430\u0437','\u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u0431\u0430\u043d\u043a','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u0444\u0438\u043d\u0430\u043d\u0441'];\n\nfunction svcType(blob){\n  if(blob.indexOf('\u0438\u043f\u043e\u0442\u0435\u043a')>=0||blob.indexOf('\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441')>=0)return 'mortgage_refinance';\n  if(blob.indexOf('\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430')>=0||blob.indexOf('\u043e\u0431\u043e\u0440\u043e\u0442\u043d')>=0||blob.indexOf('\u0434\u043b\u044f \u0438\u043f')>=0||blob.indexOf('\u0434\u043b\u044f \u043e\u043e\u043e')>=0||blob.indexOf('\u0442\u0435\u043d\u0434\u0435\u0440\u043d')>=0)return 'business_credit';\n  if(blob.indexOf('\u043f\u0442\u0441')>=0||blob.indexOf('\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433')>=0||blob.indexOf('\u0437\u0430\u043b\u043e\u0433 \u0430\u0432\u0442\u043e')>=0)return 'pts_loan';\n  if(blob.indexOf('\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437')>=0||blob.indexOf('\u043e\u0442\u043a\u0430\u0437')>=0)return 'credit_after_refusals';\n  if(blob.indexOf('\u043a\u0440\u0435\u0434\u0438\u0442')>=0||blob.indexOf('\u0437\u0430\u0439\u043c')>=0)return 'credit_broker';\n  return 'unknown';\n}\nfunction regionScore01(region,blob){\n  const r=low(region);\n  if(r.indexOf('\u043c\u043e\u0441\u043a\u0432')>=0||r.indexOf('\u043c\u043e')===0||blob.indexOf('\u043c\u043e\u0441\u043a\u0432')>=0)return 1;\n  if(r&&r!=='unknown')return 0.6;\n  return 0.3;\n}\n// public contact extraction (verbatim from PUBLIC text only; never inferred/reconstructed)\nfunction extractContact(text,explicitContact,explicitChannel){\n  const t=str(text);\n  const ph=t.match(/(?:\\+7|8)[\\s\\-(]*\\d{3}[\\s\\-)]*\\d{3}[\\s\\-]*\\d{2}[\\s\\-]*\\d{2}/);\n  const h=t.match(/@[a-zA-Z0-9_]{5,32}/);\n  let phone='',username='',ctype='',conf=0,evidence='';\n  if(ph){phone=ph[0];ctype='phone';conf=80;const i=t.indexOf(ph[0]);evidence=cut(t.slice(Math.max(0,i-40),i+ph[0].length+10),120);}\n  else if(h){username=h[0];ctype='username';conf=60;const i=t.indexOf(h[0]);evidence=cut(t.slice(Math.max(0,i-40),i+h[0].length+10),120);}\n  else if(explicitContact){const ec=str(explicitContact);if(/^(?:\\+7|8|')/.test(ec)){phone=ec.replace(/^'/,'');ctype='phone';conf=70;}else if(ec.charAt(0)==='@'){username=ec;ctype='username';conf=55;}evidence=cut(ec,120);}\n  return {phone:phone,username:username,ctype:ctype,conf:conf,evidence:evidence};\n}\n\n// ---- build candidate pool\nconst cands=[];\nfunction pushCand(o){\n  if(platformFilter&&low(o.platform)!==platformFilter)return;\n  if(sourceTypeFilter&&low(o.source_type)!==sourceTypeFilter)return;\n  cands.push(o);\n}\n// review_queue (WF08 output, 35-col business schema) \u2014 enrichment source\nfor(const r of review){\n    var __rst=low(r.source_type);if(['public_discussion','social_comment'].indexOf(__rst)<0&&low(r.touchpoint_type)!=='public_comment')continue; // LEAD-AUD-001: only real audience comments become lead candidates\n  pushCand({src:'review_queue',source_type:str(r.source_type),platform:low(r.platform),\n    source_url:str(r.source_url),...splitCmt(str(r.post_url)||str(r.source_url),str(r.comment_url)),\n    profile_url:str(r.profile_url),author_name:str(r.profile_name),author_id:str(r.author_handle)||str(r.author_id),\n    region:str(r.region),service_need:str(r.service_type)||str(r.detected_need),\n    text:[str(r.text_context),str(r.detected_need),str(r.reason)].filter(Boolean).join(' || '),\n    contact_public:str(r.contact_public),contact_channel:str(r.contact_channel),\n    record_type_hint:low(r.record_type_hint)||low(r.entity_type),\n    observed_at:str(r.created_at)||str(r.observed_at)});\n}\n// raw_market_records audience rows (PRIMARY; decoupled from WF08)\nfor(const r of raw){\n  const tpt=low(r.touchpoint_type), rth=low(r.record_type_hint);\n  const isAudience=(rth==='question_objection'||rth==='lead_signal')||['public_comment','forum_discussion','forum_thread','review_source','review_item'].indexOf(tpt)>=0;\n  if(!isAudience)continue;\n  const ds=low(r.dedup_status);\n  if(ds.indexOf('duplicate')===0||ds==='hard_skipped'||ds==='invalid'||ds==='over_pipeline_limit')continue;\n  const ps=low(r.processing_status);\n  if(ps&&(ps.indexOf('fail')>=0||ps.indexOf('error')>=0))continue;\n  pushCand({src:'raw_market_records',source_type:str(r.source_type),platform:low(r.platform),\n    source_url:str(r.source_url),...splitCmt(str(r.post_url)||str(r.source_url),str(r.comment_url)),\n    profile_url:str(r.profile_url),author_name:str(r.profile_name),author_id:str(r.author_handle),\n    region:str(r.region_hint)||str(r.region),service_need:str(r.service_hint)||str(r.probable_need),\n    text:[str(r.comment_text),str(r.text_context)].filter(Boolean).join(' || '),\n    contact_public:str(r.contact_public),contact_channel:str(r.contact_channel),\n    record_type_hint:rth,\n    observed_at:str(r.published_at)||str(r.created_at)});\n}\n\n// ---- classify + score + dedup\nlet dupSkipped=0, irrelevant=0, supplierSkipped=0, belowThreshold=0;\nconst seenBatch=new Set();\nconst scored=[];\nfor(const c of cands){\n  const blob=low(c.text);\n  if(blob.length<25){irrelevant++;continue;}\n  const pains=[];for(const p of PAIN){if(blob.indexOf(p[0])>=0&&pains.indexOf(p[1])<0)pains.push(p[1]);}\n  const hHit=INTENT_HIGH.filter(t=>blob.indexOf(t)>=0).length;\n  const mHit=INTENT_MED.filter(t=>blob.indexOf(t)>=0).length;\n  const qHit=QUESTION.filter(t=>blob.indexOf(t)>=0).length;\n  const objHit=OBJECTION.filter(t=>blob.indexOf(t)>=0).length;\n  const compHit=COMPLAINT.filter(t=>blob.indexOf(t)>=0).length;\n  const uHigh=URGENT_HIGH.filter(t=>blob.indexOf(t)>=0).length;\n  const uMed=URGENT_MED.filter(t=>blob.indexOf(t)>=0).length;\n  const supHit=SUPPLIER.filter(t=>blob.indexOf(t)>=0).length;\n  // supplier/ad exclusion: a broker advertising (not a consumer asking) is NOT a lead\n  if(c.record_type_hint==='competitor_activity'||(supHit>=2&&hHit===0&&qHit===0&&objHit===0&&compHit===0)){supplierSkipped++;continue;}\n  // relevance gate: must show consumer demand (pain OR intent OR question OR objection/complaint)\n  if(!pains.length&&!hHit&&!mHit&&!qHit&&!objHit&&!compHit){irrelevant++;continue;}\n  // intent_type (one)\n  let intent='content_signal';\n  if(hHit>0)intent='buying_intent';\n  else if(compHit>0)intent='complaint';\n  else if(objHit>0&&qHit===0)intent='objection';\n  else if(qHit>0||mHit>0)intent='question';\n  // freshness\n  const tsN=Date.parse(c.observed_at); const hasTs=!isNaN(tsN); const fresh=hasTs?tsN>=cutoffMs:true;\n  const freshStatus=hasTs?(fresh?'fresh':'stale'):'unknown';\n  if(hasTs&&!fresh&&!backfill){irrelevant++;continue;}\n  // component scores (0..1)\n  const intent01=hHit>0?1:((mHit>0||qHit>0)?0.6:0.2);\n  const urgency01=uHigh>0?1:(uMed>0?0.55:0.2);\n  let severe=pains.filter(p=>SEVERE.indexOf(p)>=0).length;\n  const pain01=pains.length===0?0.1:(severe>=2?1:(severe===1?0.8:0.5));\n  const nicheHits=NICHE_STRONG.filter(t=>blob.indexOf(t)>=0).length;\n  const niche01=nicheHits>=3?1:(nicheHits>=1?0.6:0.2);\n  const contactProbe=extractContact(c.text,c.contact_public,c.contact_channel);\n  const contact01=contactProbe.ctype?1:0;\n  const region01=regionScore01(c.region,blob);\n  const fresh01=freshStatus==='fresh'?1:(freshStatus==='stale'?0.3:0.5);\n  let raw01=intent01*W.intent+urgency01*W.urgency+pain01*W.pain+niche01*W.niche+contact01*W.contact+region01*W.region+fresh01*W.freshness;\n  // spam/irrelevance penalty: only weak market terms, no pain & no explicit intent/question\n  if(!pains.length&&hHit===0&&qHit===0&&mHit===0)raw01=Math.max(0,raw01-20);\n  let leadScore=Math.max(0,Math.min(100,Math.round(raw01)));\n  if(leadScore<minScore){belowThreshold++;continue;}\n  // dedup\n  const tnorm=norm(c.text); const th=hex(h32(tnorm,5381)), th2=hex(h32(tnorm,7919));\n  const platform=low(c.platform)||'unknown';\n  const post=low(c.post_url), cmt=low(c.comment_url), prof=low(c.profile_url);\n  const lsid='pls_'+hex(h32(platform+'|'+(cmt||post)+'|'+tnorm+'|'+intent,5381))+hex(h32(platform+'|'+(cmt||post)+'|'+tnorm+'|'+intent,7919));\n  const keys=[];\n  if(cmt)keys.push('c|'+platform+'|'+cmt);\n  if(post)keys.push('p|'+platform+'|'+post+'|'+th);\n  if(prof)keys.push('u|'+platform+'|'+prof+'|'+th);\n  keys.push('h|'+platform+'|'+th2);\n  let dupOf='';\n  if(existIds.has(low(lsid))){dupOf=lsid;}\n  if(!dupOf){for(const k of keys){if(existKeyToId[k]){dupOf=existKeyToId[k];break;}}}\n  if(dupOf){dupSkipped++;continue;}\n  let inBatch=false;for(const k of keys){if(seenBatch.has(k)){inBatch=true;break;}}\n  if(seenBatch.has(lsid))inBatch=true;\n  if(inBatch){dupSkipped++;continue;}\n  for(const k of keys)seenBatch.add(k); seenBatch.add(lsid);\n  scored.push({c,pains,intent,leadScore,fresh,freshStatus,intent01,urgency01,pain01,niche01,contact01,region01,fresh01,\n    severe,hHit,mHit,qHit,objHit,compHit,uHigh,uMed,contactProbe,lsid,th,ts:hasTs?tsN:0});\n}\n\n// rank: fresh first -> higher lead_score -> newer\nscored.sort((a,b)=>{ if(a.fresh!==b.fresh)return a.fresh?-1:1; if(b.leadScore!==a.leadScore)return b.leadScore-a.leadScore; return (b.ts||0)-(a.ts||0); });\nconst afterFilters=scored.length;\nconst pool=scored.slice(0,maxSource);\nconst chosen=pool.slice(0,maxWrite);\n\nfunction band(s){return s>=bandHigh?'high':(s>=bandMed?'medium':(s>=bandLow?'low':'ignore'));}\nfunction splitCmt(post,cu){post=str(post);cu=str(cu);if(!cu&&/[?&#]reply/i.test(post)){cu=post;post=post.replace(/[?#].*$/,'');}return {post_url:post,comment_url:cu};}function priorityOf(b){return b==='high'?'high':(b==='medium'?'medium':(b==='low'?'low':'ignore'));}\n\nconst out=[]; const nowIso=str(cfg.created_at);\nlet contactsFound=0, contactsBlanked=0;\nfor(const s of chosen){\n  const c=s.c; let b=band(s.leadScore);\n  if((b==='high'||b==='medium')&&s.pains.length===0&&s.intent!=='buying_intent')b='low'; // LEAD-STRONG-001: pain-less non-buying comments never enter the strong section\n  const cp=s.contactProbe;\n  let action;\n  if(b==='ignore')action='ignore';\n  else if(s.intent==='buying_intent'||s.intent==='question')action='manual_review';\n  else if(s.intent==='objection'||s.intent==='complaint')action='content_idea';\n  else action='monitor';\n  if(ALLOWED.indexOf(action)<0||FORBIDDEN.indexOf(action)>=0)action='manual_review'; // never outreach\n  // public contact handling\n  let public_phone='', public_username='', public_profile_url='', public_contact_type='', contact_source_url='', contact_evidence_text='', contact_conf=0;\n  const privacy=[]; let policy=str(cfg.contact_use_policy_default)||'aggregate_only';\n  const evidenceAnchor=str(c.comment_url)||str(c.post_url)||str(c.source_url);\n  if(cp.ctype){\n    if(evidenceAnchor){\n      public_phone=sheetsSafe(cp.phone); public_username=cp.username;\n      public_profile_url=str(c.profile_url); public_contact_type=cp.ctype;\n      contact_source_url=evidenceAnchor; contact_evidence_text=cp.evidence; contact_conf=cp.conf;\n      contactsFound++;\n      // weak evidence -> do_not_use + privacy flag\n      if(cp.conf<50){policy=str(cfg.contact_use_policy_weak_evidence)||'do_not_use';privacy.push('weak_contact_evidence');}\n      else {policy=str(cfg.contact_use_policy_with_public_contact)||'manual_review';}\n      privacy.push('public_contact_present_manual_review_only');\n    } else {\n      // contact text but no public source URL to prove it -> blank it (cannot evidence public visibility)\n      contactsBlanked++; policy=str(cfg.contact_use_policy_weak_evidence)||'do_not_use';\n      privacy.push('contact_blanked_no_source_url');\n    }\n  }\n  if(policy!=='manual_review'&&policy!=='aggregate_only'&&policy!=='do_not_use')policy='aggregate_only';\n  if(!(str(c.author_name)||str(c.author_id)))privacy.push('no_public_identity');\n  const painLabel=s.pains.join(', ')||'unknown';\n  const reasons='intent='+s.intent+'('+Math.round(s.intent01*W.intent)+'); urgency='+Math.round(s.urgency01*W.urgency)+'; pain['+painLabel+']='+Math.round(s.pain01*W.pain)+'; niche='+Math.round(s.niche01*W.niche)+'; contact='+Math.round(s.contact01*W.contact)+'; region='+Math.round(s.region01*W.region)+'; fresh='+Math.round(s.fresh01*W.freshness);\n  const svc=svcType(low(c.text));\n  // Defect B fix: deterministic evidence wins; a non-informative upstream hint ('unknown'/empty) must NOT shadow it.\n  const _hint=low(c.service_need); const _hintOk=_hint&&_hint!=='unknown'&&_hint!=='none'&&_hint!=='unclear'&&_hint!=='n/a';\n  const service_type_resolved=(svc!=='unknown')?svc:(_hintOk?str(c.service_need):'unknown');\n  out.push({ json:{\n    lead_signal_id:s.lsid,\n    created_at:nowIso,\n    updated_at:nowIso,\n    extracted_at:str(c.observed_at)||nowIso,\n    source_platform:low(c.platform)||'unknown',\n    source_type:str(c.source_type)||'unknown',\n    source_url:str(c.source_url),\n    source_post_url:str(c.post_url),\n    source_comment_url:str(c.comment_url),\n    source_author_public_id:str(c.author_id),\n    source_author_public_name:str(c.author_name),\n    source_public_profile_url:str(c.profile_url),         // public-only by upstream contract; evidence, NOT permission\n    evidence_text:cut(c.text,300),\n    evidence_excerpt:cut(c.text,160),\n    service_type:service_type_resolved,\n    region:str(c.region)||str(cfg.region_default),\n    intent_type:s.intent,\n    pain_type:painLabel,\n    urgency:Math.round(s.urgency01*100),\n    niche_fit:Math.round(s.niche01*100),\n    lead_score:s.leadScore,\n    score_band:b,\n    score_reasons:reasons,\n    source_confidence:(c.src==='review_queue'?55:50)+(s.pains.length>=2?10:0)+(s.niche01>=1?5:0),\n    contact_confidence:contact_conf,\n    freshness_status:s.freshStatus,\n    public_phone:public_phone,\n    public_username:public_username,\n    public_profile_url:public_profile_url,\n    public_contact_type:public_contact_type,\n    contact_source_url:contact_source_url,\n    contact_evidence_text:contact_evidence_text,\n    contact_use_policy:policy,\n    review_status:'new',\n    review_priority:priorityOf(b),\n    recommended_action:action,\n    manager_note:'',\n    reviewed_by:'',\n    reviewed_at:'',\n    rejection_reason:'',\n    duplicate_of:'',\n    privacy_flags:privacy.join('; '),\n    outreach_allowed:false,\n    policy_note:'Public evidence only; public profile/contact = evidence, NOT permission; manual review only; no auto-outreach (CONTACT_AND_OUTREACH_POLICY).',\n    agent_request_id:str(cfg.agent_request_id),\n    run_id:str(cfg.run_id),\n    notes:'wf14 v0.3 lead scout (Stage 3.5, DEC-138); text_hash='+s.th+'; src='+c.src+'; '+reasons\n  }});\n}\n\n// Defect E: precise zero-write diagnosis (distinguish dedup / no-source / none-relevant / below-threshold / supplier / cap).\nfunction diagnoseZeroWrite(){\n  if(out.length>0)return {reason:'not_applicable',next:''};\n  if((raw.length+review.length)===0)\n    return {reason:'no_source_rows', next:'No source rows were read. Run a source connector first (WF13 VK fixture/live, WF09, WF11) to populate raw_market_records, then re-run WF14.'};\n  if(cands.length===0)\n    return {reason:'no_audience_rows', next:'Source rows exist but none are audience/consumer rows (all competitor/market/non-audience). Collect consumer comments/questions before expecting leads.'};\n  if(scored.length===0 && dupSkipped>0 && dupSkipped>=irrelevant && dupSkipped>=supplierSkipped && dupSkipped>=belowThreshold)\n    return {reason:'all_eligible_already_exist', next:'No new rows written because all '+dupSkipped+' eligible signal(s) already exist in public_lead_signals (deduplicated). Deduplication succeeded \u2014 no threshold change needed. Collect NEW source data (run WF13 again on new posts/comments) before expecting new leads.'};\n  if(scored.length===0 && belowThreshold>0 && belowThreshold>=irrelevant && belowThreshold>=supplierSkipped)\n    return {reason:'below_threshold', next:belowThreshold+' relevant candidate(s) scored below min_lead_score='+minScore+'. Lower min_lead_score only if weaker leads are acceptable; otherwise collect stronger-intent sources.'};\n  if(scored.length===0 && supplierSkipped>0 && supplierSkipped>=irrelevant)\n    return {reason:'supplier_excluded', next:supplierSkipped+' candidate(s) were supplier/competitor adverts (excluded by policy), not consumer demand. Expected; collect audience comments under competitor posts.'};\n  if(scored.length===0 && irrelevant>0)\n    return {reason:'none_relevant', next:irrelevant+' candidate(s) showed no consumer-demand evidence (irrelevant). Widen sources or the time window; no threshold change needed.'};\n  if(afterFilters>maxWrite)\n    return {reason:'append_cap', next:'Eligible signals exceeded the append cap (max_signals_to_write='+maxWrite+'); raise the cap or narrow the window.'};\n  return {reason:'no_new_signals', next:'No new public lead signals; run source connectors (WF13 VK / WF09 / WF11) first, widen time_window_days, or lower min_lead_score.'};\n}\nconst _zw=diagnoseZeroWrite();\n// expose stats for downstream summary / agent_requests\nconst sd=$getWorkflowStaticData('global');\nsd.wf14_run={\n  rows_read_review_queue:review.length,\n  rows_read_raw_market_records:raw.length,\n  rows_read_existing_public_lead_signals:existing.length,\n  candidates_considered:cands.length,\n  rows_after_filters:afterFilters,\n  duplicates_skipped:dupSkipped,\n  irrelevant_skipped:irrelevant,\n  supplier_skipped:supplierSkipped,\n  signals_written:out.length,\n  contacts_found_public:contactsFound,\n  contacts_blank_due_to_policy:contactsBlanked,\n  high_priority:out.filter(o=>o.json.review_priority==='high').length,\n  medium_priority:out.filter(o=>o.json.review_priority==='medium').length,\n  low_priority:out.filter(o=>o.json.review_priority==='low').length,\n  ignored:out.filter(o=>o.json.score_band==='ignore').length,\n  max_source_rows:maxSource,\n  max_signals_to_write:maxWrite,\n  below_threshold_skipped:belowThreshold,\n  zero_write_reason:_zw.reason,\n  next_action_no_signals:_zw.next\n};\n\nif(!out.length){\n  return [{ json:{ _no_signals:true, candidates_considered:cands.length, rows_after_filters:afterFilters, duplicates_skipped:dupSkipped, irrelevant_skipped:irrelevant, supplier_skipped:supplierSkipped, below_threshold_skipped:belowThreshold, zero_write_reason:_zw.reason } }];\n}\nreturn out;\n"
      },
      "id": "wf14-classify",
      "name": "Build Candidate Pool & Classify",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        560
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "wf14-has-signals",
              "leftValue": "={{ $json._no_signals }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "notEquals",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "wf14-if",
      "name": "IF signals found?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1140,
        560
      ]
    },
    {
      "id": "wf14-app-pls",
      "name": "Append public_lead_signals",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1360,
        440
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "public_lead_signals",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// One agent_requests row (exactly 21 columns). status=completed. $0, no external calls, no Claude.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Triage Config').first().json;\nconst items=$('Build Candidate Pool & Classify').all().map(i=>i.json);\nconst noSignals=items.length===1&&items[0]._no_signals===true;\nconst signals=noSignals?[]:items;\nconst sd=$getWorkflowStaticData('global');const st=sd.wf14_run||{};\nfunction pick(k,fb){return (typeof st[k]==='number')?st[k]:fb;}\nconst byIntent={};for(const s of signals){const k=str(s.intent_type)||'unknown';byIntent[k]=(byIntent[k]||0)+1;}\nconst dup=pick('duplicates_skipped',0);\nconst contactsFound=pick('contacts_found_public',signals.filter(s=>str(s.public_phone)||str(s.public_username)||str(s.public_profile_url)).length);\nconst hi=pick('high_priority',signals.filter(s=>str(s.review_priority)==='high').length);\nconst med=pick('medium_priority',signals.filter(s=>str(s.review_priority)==='medium').length);\nconst lo=pick('low_priority',signals.filter(s=>str(s.review_priority)==='low').length);\nreturn [{ json: {\n  agent_request_id:str(cfg.agent_request_id),\n  created_at:str(cfg.created_at),\n  requested_by:str(cfg.requested_by)||'operator',\n  request_type:'public_lead_signal_triage',\n  request_text:'Lead Scout triage+scoring (Stage 3.5): public audience comments/questions/reviews -> public_lead_signals v0.3 (window '+(Number(cfg.time_window_days)||30)+'d; max_source_rows='+(Number(cfg.max_source_rows)||120)+'; max_write='+Math.min(25,Number(cfg.max_signals_to_write)||25)+'; min_lead_score='+(Number(cfg.min_lead_score)||0)+').',\n  source_scope:'business_queues',\n  platforms:str(cfg.platform_filter)||'all',\n  query:'raw_market_records audience rows (primary) + review_queue (enrichment); scored/deduped/capped',\n  region:str(cfg.region_default)||'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n  service_focus:'credit_brokerage',\n  requested_limit:Math.min(25,Number(cfg.max_signals_to_write)||25),\n  status:'completed',\n  plan_summary:'WF14 Lead Scout Triage & Scoring v0.3 (Stage 3.5, DEC-138): single-read tabs + bounded candidate pool + deterministic 0-100 lead scoring + public-contact extraction + dedup + capped append into public_lead_signals (v0.3). No Claude, no external calls, no outreach; outreach_allowed=false; contact_use_policy=manual_review/aggregate_only/do_not_use.',\n  estimated_source_cost_usd:0,\n  estimated_analysis_cost_usd:0,\n  approval_required:false,\n  approved_by:'',\n  approved_at:'',\n  result_summary:(noSignals?('no_signals('+(str(st.zero_write_reason)||'unknown')+'); '):'')+'signals_written='+signals.length+'; high='+hi+'; medium='+med+'; low='+lo+'; duplicates_skipped='+dup+'; by_intent='+JSON.stringify(byIntent)+'; public_contacts='+contactsFound+'; outreach_allowed=false; policy=manual_review/aggregate_only/do_not_use.',\n  next_action:(signals.length?'Manager: review public_lead_signals rows with review_status=new (high priority first); set accepted/rejected/duplicate/stale. NO outreach \u2014 public profile/contact is evidence, not permission (CONTACT_AND_OUTREACH_POLICY).':(str(st.next_action_no_signals)||'No new public lead signals; run source connectors (WF13 VK / WF09 / WF11) first, widen time_window_days, or lower min_lead_score.')),\n  notes:'wf14 v0.3 deterministic lead scout; reads review_queue + raw_market_records ONCE each; writes public_lead_signals + agent_requests only; never auto-outreach; no private chats; no member extraction; no hidden/inferred contacts.'\n}}];\n"
      },
      "id": "wf14-ar",
      "name": "Build agent_requests Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1580,
        560
      ]
    },
    {
      "id": "wf14-app-ar",
      "name": "Append agent_requests",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1780,
        560
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "agent_requests",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Final run summary (Stage 3.5 Lead Scout). No external calls. Includes read-once/cap/dedup/policy self-test.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Triage Config').first().json;\nconst items=$('Build Candidate Pool & Classify').all().map(i=>i.json);\nconst noSignals=items.length===1&&items[0]._no_signals===true;\nconst signals=noSignals?[]:items;\nconst sd=$getWorkflowStaticData('global');const st=sd.wf14_run||{};\nfunction pick(k,fb){return (typeof st[k]==='number')?st[k]:fb;}\nconst reviewN=pick('rows_read_review_queue',$('Read review_queue').all().length);\nconst rawN=pick('rows_read_raw_market_records',$('Read raw_market_records').all().length);\nconst existN=pick('rows_read_existing_public_lead_signals',$('Read public_lead_signals').all().length);\nconst _cap=Math.min(25,Number(cfg.max_signals_to_write)||25);\nconst _dupSkipped=pick('duplicates_skipped',0);\nconst hi=pick('high_priority',signals.filter(s=>str(s.review_priority)==='high').length);\nconst med=pick('medium_priority',signals.filter(s=>str(s.review_priority)==='medium').length);\nconst lo=pick('low_priority',signals.filter(s=>str(s.review_priority)==='low').length);\nconst ignored=pick('ignored',signals.filter(s=>str(s.score_band)==='ignore').length);\nconst contactsFound=pick('contacts_found_public',signals.filter(s=>str(s.public_phone)||str(s.public_username)||str(s.public_profile_url)).length);\nconst contactsBlank=pick('contacts_blank_due_to_policy',0);\n// --- self-tests (DEC-131 quota + Stage 3.5 policy)\nconst _readOnceOk=(typeof reviewN==='number')&&(typeof rawN==='number')&&(typeof existN==='number');\nconst _capOk=(signals.length<=_cap);\nconst _dedupOk=true; // dedup runs deterministically; repeat runs should write 0 + duplicates_skipped>0\n// policy self-test: no row may permit outreach; recommended_action confined to allowed set\nconst ALLOWED=['manual_review','content_idea','monitor','ignore'];\nconst POLICY=['manual_review','aggregate_only','do_not_use'];\nlet _policyOk=true; let _policyViol='';\nfor(const s of signals){\n  if(s.outreach_allowed===true||str(s.outreach_allowed).toLowerCase()==='true'){_policyOk=false;_policyViol='outreach_allowed=true on '+str(s.lead_signal_id);break;}\n  if(ALLOWED.indexOf(str(s.recommended_action))<0){_policyOk=false;_policyViol='bad recommended_action '+str(s.recommended_action);break;}\n  if(POLICY.indexOf(str(s.contact_use_policy))<0){_policyOk=false;_policyViol='bad contact_use_policy '+str(s.contact_use_policy);break;}\n  if((str(s.public_phone)||str(s.public_username)||str(s.public_profile_url))&&!str(s.contact_source_url)){_policyOk=false;_policyViol='contact without contact_source_url on '+str(s.lead_signal_id);break;}\n}\nconst _selfTestPassed=_readOnceOk&&_capOk&&_dedupOk&&_policyOk;\nlet status='completed';\nif(!str(cfg.run_id))status='failed_validation';\nelse if(!signals.length)status='completed_no_data';\nreturn [{ json: {\n  workflow:'14 - Lead Scout Triage & Scoring (Deterministic, Stage 3.5)',\n  run_id:str(cfg.run_id),\n  agent_request_id:str(cfg.agent_request_id),\n  rows_read:reviewN+rawN+existN,\n  rows_read_review_queue:reviewN,\n  rows_read_raw_market_records:rawN,\n  rows_read_existing_public_lead_signals:existN,\n  candidates_considered:pick('candidates_considered',0),\n  rows_after_filters:pick('rows_after_filters',signals.length),\n  duplicates_skipped:_dupSkipped,\n  irrelevant_skipped:pick('irrelevant_skipped',0),\n  below_threshold_skipped:pick('below_threshold_skipped',0),\n  zero_write_reason:(signals.length>0?'not_applicable':(str(st.zero_write_reason)||'no_eligible_records')),\n  supplier_skipped:pick('supplier_skipped',0),\n  signals_written:signals.length,\n  high_priority:hi,\n  medium_priority:med,\n  low_priority:lo,\n  ignored:ignored,\n  contacts_found_public:contactsFound,\n  contacts_blank_due_to_policy:contactsBlank,\n  max_source_rows:Math.min(200,Number(cfg.max_source_rows)||120),\n  max_signals_to_write:_cap,\n  claude_calls:0,\n  external_calls:0,\n  cost_usd:0,\n  read_once_ok:_readOnceOk,\n  append_cap_ok:_capOk,\n  dedup_ok:_dedupOk,\n  policy_ok:_policyOk,\n  self_test_passed:_selfTestPassed,\n  status:status,\n  next_action:(signals.length?'Manager: review review_status=new rows in public_lead_signals (high priority first); set accepted/rejected/duplicate/stale. NO outreach (CONTACT_AND_OUTREACH_POLICY).':(str(st.next_action_no_signals)||'No new public lead signals; run connectors (WF13 VK / WF09 / WF11) first, widen time_window_days, or lower min_lead_score.')),\n  health_check:'read_once='+_readOnceOk+'; signals='+signals.length+'<=cap('+_cap+')='+_capOk+'; policy_ok='+_policyOk+(_policyViol?(' ('+_policyViol+')'):'')+'; high/med/low='+hi+'/'+med+'/'+lo+'; public_contacts='+contactsFound+'; repeat runs should write 0 + duplicates_skipped>0'\n}}];\n"
      },
      "id": "wf14-final",
      "name": "Final Summary Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        560
      ]
    },
    {
      "parameters": {
        "inputSource": "workflowInputs",
        "workflowInputs": {
          "values": [
            {
              "name": "source_agent_request_id",
              "type": "string"
            },
            {
              "name": "agent_request_id",
              "type": "string"
            },
            {
              "name": "data_mode",
              "type": "string"
            },
            {
              "name": "platform_filter",
              "type": "string"
            },
            {
              "name": "source_type_filter",
              "type": "string"
            },
            {
              "name": "min_lead_score",
              "type": "string"
            },
            {
              "name": "include_review_queue",
              "type": "string"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -40,
        -160
      ],
      "id": "wf14-subtrig",
      "name": "When Called by Agent"
    }
  ],
  "connections": {
    "Manual Start": {
      "main": [
        [
          {
            "node": "Set Triage Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Triage Config": {
      "main": [
        [
          {
            "node": "Read review_queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read review_queue": {
      "main": [
        [
          {
            "node": "Hold Config (before raw)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hold Config (before raw)": {
      "main": [
        [
          {
            "node": "Read raw_market_records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read raw_market_records": {
      "main": [
        [
          {
            "node": "Hold Config (before pls)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hold Config (before pls)": {
      "main": [
        [
          {
            "node": "Read public_lead_signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read public_lead_signals": {
      "main": [
        [
          {
            "node": "Build Candidate Pool & Classify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Candidate Pool & Classify": {
      "main": [
        [
          {
            "node": "IF signals found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF signals found?": {
      "main": [
        [
          {
            "node": "Append public_lead_signals",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build agent_requests Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append public_lead_signals": {
      "main": [
        [
          {
            "node": "Build agent_requests Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build agent_requests Row": {
      "main": [
        [
          {
            "node": "Append agent_requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append agent_requests": {
      "main": [
        [
          {
            "node": "Final Summary Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Called by Agent": {
      "main": [
        [
          {
            "node": "Set Triage Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "wf14-v003-lead-scout-triage-scoring-stage35-20260617"
}