{
  "name": "10 - Competitor Audience Intelligence Aggregator",
  "nodes": [
    {
      "id": "aa100000-0000-0000-0000-0000000000n1",
      "name": "Overview Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -460,
        -300
      ],
      "parameters": {
        "content": "## 10 \u2014 Competitor/Audience Intelligence Aggregator v0.3 \u2014 DEC-104/106/107/108/127\n\nWF08 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u00ab\u0447\u0442\u043e \u044d\u0442\u043e \u0437\u0430 \u043e\u0434\u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c?\u00bb. WF10 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u00ab\u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043d\u0430 \u0440\u044b\u043d\u043a\u0435 \u0432 \u0446\u0435\u043b\u043e\u043c?\u00bb.\n\n\u0427\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 (\u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u041e, $0, \u0431\u0435\u0437 LLM/Apify/Firecrawl):\n1. \u0420\u0443\u0447\u043d\u043e\u0439 \u0441\u0442\u0430\u0440\u0442 \u2192 Set Aggregator Config (\u043e\u043a\u043d\u043e 30 \u0434\u043d\u0435\u0439; \u0444\u0438\u043b\u044c\u0442\u0440\u044b niche/platform/region/service_type).\n2. \u0427\u0438\u0442\u0430\u0435\u0442 \u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043a\u0438: monitor_queue, content_queue, review_queue (+ source_confidence_rules \u0434\u043b\u044f seed-\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438).\n3. Aggregate Market Intelligence: \u0444\u0438\u043b\u044c\u0442\u0440 \u043f\u043e \u043e\u043a\u043d\u0443/\u0444\u0438\u043b\u044c\u0442\u0440\u0430\u043c \u2192 \u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (company_name \u2192 profile_name \u2192 offer_text+platform \u2192 listing id \u0438\u0437 source_url) \u2192 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435 \u043e\u0444\u0444\u0435\u0440\u043e\u0432/\u0446\u0435\u043d/\u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0438/\u0431\u043e\u043b\u0435\u0439 \u2192 \u043f\u043e\u0434\u0441\u0447\u0451\u0442 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u044e\u0449\u0438\u0445\u0441\u044f \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0445 \u0443\u0433\u043b\u043e\u0432 (\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c, \u0446\u0435\u043d\u043e\u0432\u043e\u0439 \u044f\u043a\u043e\u0440\u044c, \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, \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432, \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418, \u0431\u0438\u0437\u043d\u0435\u0441-\u0444\u0438\u043d\u0430\u043d\u0441\u044b, \u0438\u043f\u043e\u0442\u0435\u043a\u0430/\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441, \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438).\n4. \u041f\u0438\u0448\u0435\u0442: competitor_profiles, market_angles, audience_activity_signals (\u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b, \u0431\u0435\u0437 \u0430\u0432\u0442\u043e\u0440\u043e\u0432/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432), content_positioning_plan (1 \u0441\u0442\u0440\u043e\u043a\u0430/\u043f\u0440\u043e\u0433\u043e\u043d), source_confidence_rules (seed \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0432\u043a\u043b\u0430\u0434\u043a\u0430 \u043f\u0443\u0441\u0442\u0430), agent_requests (1 \u0441\u0442\u0440\u043e\u043a\u0430, status=completed).\n\n\u0427\u0415\u0413\u041e \u041d\u0415 \u0414\u0415\u041b\u0410\u0415\u0422: \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 API; \u043d\u0435 \u0441\u043a\u0440\u0435\u0439\u043f\u0438\u0442; \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 Claude; \u043d\u0435 \u0432\u044b\u0434\u0443\u043c\u044b\u0432\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b; \u043d\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442 outreach (DEC-098); \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0435\u0442 raw_market_records/registry/\u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. active=false; \u0437\u0430\u043f\u0443\u0441\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e. Niche-\u0441\u043b\u043e\u0432\u0430\u0440\u044c v0.1 \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d (credit_brokerage) \u2014 \u0432\u044b\u043d\u043e\u0441 \u0432 niche packs \u043f\u043e NICHE_PACK_SYSTEM_PLAN.\n\n**v0.2 (2026-06-12):** 1) no-data guard \u2014 \u043f\u0440\u0438 rows_after_filters=0 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0439 no_data \u043f\u043b\u0430\u043d (\u043f\u0443\u0441\u0442\u044b\u0435 top_angles/posts/ads/faq/counterarguments/lead_magnets, source_evidence=rows=0, next_action=no_data) + agent_requests result_summary \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0441 no_data; 2) entity resolution \u2014 company_name \u2192 profile_url \u2192 canonical listing id (source_url) \u2192 profile_name \u2192 offer+platform (fallback) \u2014 \u043c\u0435\u043d\u044c\u0448\u0435 \u0434\u0443\u0431\u043b\u0435\u0439 (unnamed) \u0434\u043b\u044f \u043e\u0434\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u044f; 3) source_mix \u043c\u0435\u0442\u043a\u0430 \u0432 notes/result_summary: mixed: live + historical/manual + web pipeline \u2014 \u043e\u0442\u0447\u0451\u0442\u044b \u043d\u0435 \u0432\u044b\u0434\u0430\u044e\u0442 \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430 \u0441\u0432\u0435\u0436\u0438\u0439 live-\u0441\u0431\u043e\u0440.\n\n**v0.3 (DEC-127):** audience_activity_signals \u0441\u0447\u0438\u0442\u0430\u0435\u0442 objection_count \u043f\u043e \u0441\u043b\u043e\u0432\u0430\u0440\u044e \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0438\u044f (\u0431\u043e\u044e\u0441\u044c \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432 / \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u0430 / \u0431\u0435\u0440\u0443\u0442 \u0434\u0435\u043d\u044c\u0433\u0438 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430\u044e\u0442 / \u043e\u0431\u043c\u0430\u043d / \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u044f / \u0434\u043e\u0433\u043e\u0432\u043e\u0440) \u0422\u041e\u041b\u042c\u041a\u041e \u043f\u043e \u0441\u0442\u0440\u043e\u043a\u0430\u043c review_queue (\u0433\u043e\u043b\u043e\u0441 \u0430\u0443\u0434\u0438\u0442\u043e\u0440\u0438\u0438 \u2014 \u0440\u0435\u043a\u043b\u0430\u043c\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u043d\u0435 \u0438\u043d\u0444\u043b\u0438\u0440\u0443\u0435\u0442 \u0441\u0447\u0451\u0442\u0447\u0438\u043a); top_pains \u0443\u043a\u0440\u0443\u043f\u043d\u0435\u043d\u044b: \u00ab\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418\u00bb, \u00ab\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432\u00bb; blob \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 comment_text. \u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 aggregate-only \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439.",
        "height": 940,
        "width": 620,
        "color": 4
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000n2",
      "name": "Setup and Test Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        200,
        -300
      ],
      "parameters": {
        "content": "## \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438 \u0442\u0435\u0441\u0442\n\n1. \u041d\u0415 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c (active=false).\n2. \u0421\u041e\u0417\u0414\u0410\u0422\u042c 5 \u043d\u043e\u0432\u044b\u0445 \u0432\u043a\u043b\u0430\u0434\u043e\u043a \u0441 \u043a\u043e\u043b\u043e\u043d\u043a\u0430\u043c\u0438 \u0438\u0437 docs/WF10_TABLE_SCHEMAS.md: competitor_profiles (17), market_angles (9), audience_activity_signals (14), content_positioning_plan (12), source_confidence_rules (5).\n3. \u041f\u043e\u0441\u043b\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b Google Sheets \u043d\u0430 \u0412\u0421\u0415\u0425 sheet-\u043d\u043e\u0434\u0430\u0445 (4 read + 5 append + agent_requests) \u0438 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c PASTE_SPREADSHEET_ID_HERE \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 ID.\n4. \u0422\u0435\u0441\u0442 1 (\u043f\u0435\u0440\u0432\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d): Execute once \u2192 competitor_profiles \u22651 (\u0438\u0437 live-\u0441\u0442\u0440\u043e\u043a WF09\u2192WF08), market_angles \u22651, audience_activity_signals = \u043f\u043e 1 \u043d\u0430 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0443, content_positioning_plan +1, source_confidence_rules +7 (seed), agent_requests +1 (completed). \u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c $0 (0 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0432\u044b\u0437\u043e\u0432\u043e\u0432).\n5. \u0422\u0435\u0441\u0442 2 (\u043f\u043e\u0432\u0442\u043e\u0440): source_confidence_rules +0 (seed \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438 \u043f\u0443\u0441\u0442\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0435); \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u043d\u043e\u0432\u044b\u0439 snapshot (append-only v0.1; upsert \u2014 v0.2).\n6. \u041e\u043a\u043d\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 30 \u0434\u043d\u0435\u0439; \u043f\u0443\u0441\u0442\u043e\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u2192 Final Summary \u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0435\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u043e\u043a\u043d\u043e/\u0444\u0438\u043b\u044c\u0442\u0440\u044b.\n\n\u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c: $0 \u0432\u0441\u0435\u0433\u0434\u0430 (\u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u044f, \u0431\u0435\u0437 LLM).\n7. \u0422\u0435\u0441\u0442 no-data (v0.2): \u0437\u0430\u0434\u0430\u0442\u044c \u0437\u0430\u0432\u0435\u0434\u043e\u043c\u043e \u043f\u0443\u0441\u0442\u043e\u0439 \u0444\u0438\u043b\u044c\u0442\u0440 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 region_filter='\u041d\u0438\u0433\u0434\u0435\u0433\u0440\u0430\u0434') \u2192 Execute once \u2192 competitor_profiles/market_angles/audience_activity_signals +0; content_positioning_plan +1 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0439 no_data (\u0432\u0441\u0435 \u0441\u043f\u0438\u0441\u043a\u0438 \u043f\u0443\u0441\u0442\u044b\u0435, source_evidence=rows=0); agent_requests result_summary \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0441 'no_data;'. \u0412\u0435\u0440\u043d\u0443\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440 \u043f\u043e\u0441\u043b\u0435 \u0442\u0435\u0441\u0442\u0430.\n8. \u0422\u0435\u0441\u0442 v0.3 (\u043f\u043e\u0441\u043b\u0435 WF13->WF08): \u0441\u0442\u0440\u043e\u043a\u0430 audience_activity_signals \u0434\u043b\u044f vk: question_count>=2, objection_count>=1, buying_intent_count>=1; top_pains \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u00ab\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432\u00bb, \u00ab\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418\u00bb, \u00ab\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432\u00bb.",
        "height": 860,
        "width": 580,
        "color": 5
      }
    },
    {
      "id": "aa100000-0000-0000-0000-000000000001",
      "name": "Manual Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -380,
        520
      ],
      "parameters": {}
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000cf",
      "name": "Set Aggregator Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -160,
        520
      ],
      "parameters": {
        "jsCode": "// WF10 aggregator config. Deterministic ids; MSK timestamps; no external calls, no LLM (DEC-104).\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 now=moscowIsoNow();\nconst stamp=moscowStamp();\nvar __cfg = {\n  time_window_days:30,\n  enable_historical_comparison:false, // \u00a72 historical trend baseline is OPT-IN, never automatic contamination\n  niche_id:'credit_brokerage',\n  platform_filter:'',\n  region_filter:'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n  service_type_filter:'',\n  max_examples_per_item:3,\n  write_confidence_seed_rules_if_empty:true,\n  requested_by:'operator',\n  request_type:'market_intelligence_aggregation',\n  spreadsheet_id_placeholder:'PASTE_SPREADSHEET_ID',\n  // --- Stage C Closure Patch 2: strict run isolation + WF16 source_health enforcement (S3-D5, \u00a710) ---\n  source_run_id_filter:'',\n  agent_request_id_filter:'',\n  data_mode_filter:'',          // '', live, fixture, manual_test\n  report_eligible_filter:'',    // '', 'true', 'false'\n  quality_status_filter:'',     // '', healthy, degraded, quarantined\n  review_status_filter:'',      // '', pending, accepted, ...\n  enforce_source_health:true,   // default ON: drop rows whose source run is not report-eligible\n  require_source_health:true,   // \u00a72 PRODUCTION fail-closed: a row lacking a source_health match is EXCLUDED (set false only for dev/back-compat)\n  allow_degraded_report:false,  // explicit operator opt-in to include degraded runs (with quality warning)\n  allow_fixture_report:false,   // explicit operator opt-in to include fixture/manual_test runs (watermark)\n  allow_unverified_source:false, // PRODUCTION fail-closed: records with no source_health match AND no live/healthy self-attestation are excluded; set true ONLY for non-production/dev runs\n  agent_request_id:'wf10_req_'+stamp,\n  run_id:'wf10_'+stamp,\n  created_at:now\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_filter','data_mode');\n__ov('platform_filter','platform_filter');\n__ov('region_filter','region_filter');\n__ov('service_type_filter','service_type_filter');\n__ov('report_eligible_filter','report_eligible_filter');\n__ov('quality_status_filter','quality_status_filter');\n__ov('review_status_filter','review_status_filter');\n__ov('time_window_days','time_window_days');\n__ov('niche_id','niche_id');\nreturn [{ json: __cfg }];"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000r1",
      "name": "Read monitor_queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        60,
        520
      ],
      "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": "monitor_queue",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000r2",
      "name": "Read content_queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        280,
        520
      ],
      "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": "content_queue",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000r3",
      "name": "Read review_queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        500,
        520
      ],
      "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": "review_queue",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000r4",
      "name": "Read source_confidence_rules",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        720,
        520
      ],
      "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": "source_confidence_rules",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000ag",
      "name": "Aggregate Market Intelligence",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        520
      ],
      "parameters": {
        "jsCode": "function 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;}\n// --- RUN-LINEAGE-001: embedded n8n/lib/run_lineage.js (drift-proof; test asserts equality) ---\n// run_lineage.js \u2014 RUN-LINEAGE-001. ONE resolver for \"which run/request/owner does this row belong to?\".\n//\n// This defect class has now cost five separate live failures, each one a silent zero-row report:\n//   ISO-ARID-001  \u2014 WF10 strict-compared an absent agent_request_id            (28 -> 0 rows)\n//   ISO-RUNID-001 \u2014 WF10 read source_run_id||agent_request_id; WF04 writes run_id (308 -> 0 rows)\n//   data_mode     \u2014 WF10 strict-compared an empty data_mode                    (all rows dropped)\n//   no_lineage    \u2014 report_gate reads ONLY source_run_id; queue rows carry run_id (row excluded)\n//   region        \u2014 an inferred default overrode an explicit user scope        (fixed in scope_policy)\n// Every one was the same shape: a consumer strict-compares a field its producer never populates, and the pipeline\n// silently returns nothing while telling the user there is no data. The cure is not another local fallback in the\n// next workflow \u2014 it is ONE resolver every consumer shares.\n//\n// Rules:\n//   * an EMPTY optional field is not a mismatch when another canonical field carries the same identity;\n//   * owner isolation is mandatory and is NEVER relaxed by this resolver;\n//   * the current run may not be satisfied by an unrelated historical row;\n//   * malformed/absent lineage fails safely AND is observable (resolved_from tells you what matched).\n//\n// Embeddable: unique rl*-prefixed names, no cross-lib require.\n\nfunction rlStr(v) { return v == null ? '' : String(v).trim(); }\nfunction rlLow(v) { return rlStr(v).toLowerCase(); }\n\n// A source run id is `<request>::<source>::<slot>`; its family root is the request id.\nfunction rlFamilyRoot(id) {\n  var s = rlStr(id);\n  var i = s.indexOf('::');\n  return i > 0 ? s.slice(0, i) : s;\n}\n\n// resolveRunLineage(row) -> { source_run_id, agent_request_id, run_id, family_root, owner_user_id, data_mode,\n//                             source_id, has_lineage, resolved_from }\n// ONLY the explicitly supported canonical/legacy fields are consulted \u2014 in one documented order.\nfunction resolveRunLineage(row) {\n  row = row || {};\n  var srid = rlStr(row.source_run_id);\n  var arid = rlStr(row.agent_request_id);\n  var rid = rlStr(row.run_id);\n  var from = [];\n  if (srid) from.push('source_run_id');\n  if (arid) from.push('agent_request_id');\n  if (rid) from.push('run_id');\n\n  // The most specific run identity available. WF04's queue rows carry it ONLY in run_id.\n  var sourceRun = srid || rid || '';\n  // The request family: an explicit agent_request_id wins; otherwise derive it from whichever run id we have.\n  var request = arid || rlFamilyRoot(srid || rid) || '';\n  var family = rlFamilyRoot(sourceRun || request);\n\n  return {\n    source_run_id: sourceRun,\n    agent_request_id: request,\n    run_id: rid,\n    family_root: family,\n    owner_user_id: rlStr(row.owner_user_id),\n    data_mode: rlLow(row.data_mode),\n    source_id: rlStr(row.source_url || row.normalized_source_url || row.source_id || row.source_key),\n    has_lineage: !!(sourceRun || request),\n    resolved_from: from.join('|') || 'none'\n  };\n}\n\n// Does `row` belong to the run identified by `filter`? `filter` may be a request id (req_x) or a source run id\n// (req_x::website::a1); a row matches its own request family either way.\nfunction rlFamilyMatch(rowLineage, filter) {\n  var f = rlStr(filter);\n  if (!f) return true;                       // no filter = no constraint\n  if (!rowLineage || !rowLineage.has_lineage) return false;   // fail safe, but ONLY when genuinely absent\n  var ids = [rowLineage.source_run_id, rowLineage.agent_request_id, rowLineage.run_id].filter(Boolean);\n  for (var i = 0; i < ids.length; i++) {\n    var id = ids[i];\n    if (id === f) return true;\n    if (id.indexOf(f + '::') === 0) return true;   // row is a source run inside the requested family\n    if (f.indexOf(id + '::') === 0) return true;   // filter is a source run inside the row's family\n  }\n  return false;\n}\n\n// Owner isolation. An unowned (system) row is visible to its own run; a row owned by SOMEONE ELSE never is.\nfunction rlOwnerMatch(rowLineage, owner) {\n  var o = rlStr(owner);\n  if (!o) return true;\n  var ro = rlStr(rowLineage && rowLineage.owner_user_id);\n  if (!ro) return true;          // unowned/system row \u2014 isolation is enforced by the run family above\n  return ro === o;\n}\n\n// The one predicate a consumer should call: does this row belong to THIS run, for THIS owner?\n// opts: { source_run_id_filter, agent_request_id_filter, owner_user_id, data_mode_filter }\nfunction rlRowInScope(row, opts) {\n  opts = opts || {};\n  var lin = resolveRunLineage(row);\n  if (!rlOwnerMatch(lin, opts.owner_user_id)) return { in_scope: false, reason: 'owner_mismatch', lineage: lin };\n  if (rlStr(opts.source_run_id_filter) && !rlFamilyMatch(lin, opts.source_run_id_filter)) return { in_scope: false, reason: 'source_run_mismatch', lineage: lin };\n  if (rlStr(opts.agent_request_id_filter) && !rlFamilyMatch(lin, opts.agent_request_id_filter)) return { in_scope: false, reason: 'request_mismatch', lineage: lin };\n  // ABSENT is not a MISMATCH: a row that never recorded a data_mode is not evidence of the wrong mode.\n  if (rlStr(opts.data_mode_filter) && lin.data_mode && lin.data_mode !== rlLow(opts.data_mode_filter)) {\n    return { in_scope: false, reason: 'data_mode_mismatch', lineage: lin };\n  }\n  return { in_scope: true, reason: '', lineage: lin };\n}\n// --- end embedded run_lineage ---\n// --- Stage C Closure Patch 2: embedded n8n/lib/report_gate.js (drift-proof; test asserts equality) ---\n// report_gate.js \u2014 Stage C Closure Patch 2: the source_health gate consumed by WF10 + WF12.\n//\n// Pure, deterministic, NO network, $0. Turns a set of WF16 `source_health` rows into the run-level\n// eligibility decisions that WF10 (aggregation) and WF12 (stakeholder report) MUST enforce before a run\n// can appear as production market intelligence. WF10/WF12 each embed a mirror of these functions; the\n// offline harness runs the real node code and asserts it equals this library (drift-proof, like WF16).\n//\n// A source run is EXCLUDED by default when any of these hold (section 4 of the closure brief):\n//   report_eligible=false, quality_status=quarantined, data_mode=fixture, data_mode=manual_test,\n//   review_status=pending, semantic validation failed, stale beyond the freshness window,\n//   no compatible baseline (for trend comparison only \u2014 handled by selectBaseline).\n// Degraded data is excluded by default and may be included ONLY through an explicit operator opt-in\n// (allow_degraded_report=true), and then it MUST carry a visible quality warning. Fixture / manual_test\n// data may be included ONLY when the operator explicitly generates a fixture report (allow_fixture_report)\n// and then a TEST/FIXTURE watermark MUST be shown.\n\n// RUN-LINEAGE-001: the ONE resolver every consumer shares (destructured so the embed step inlines it).\n\nfunction str(v) { return v == null ? '' : String(v).trim(); }\nfunction low(v) { return str(v).toLowerCase(); }\nfunction bool(v) {\n  if (v === true) return true;\n  if (v === false) return false;\n  const s = low(v);\n  return s === 'true' || s === 'yes' || s === '1';\n}\n\n// Run-level exclusion reasons we surface to the operator (stable enum).\nconst EXCLUDE = {\n  FIXTURE: 'data_mode=fixture',\n  MANUAL_TEST: 'data_mode=manual_test',\n  QUARANTINED: 'quality_status=quarantined',\n  PENDING: 'review_status=pending',\n  SEMANTIC: 'semantic_validation_failed',\n  STALE: 'stale_source',\n  NOT_ELIGIBLE: 'report_eligible=false',\n  DEGRADED_NO_OPTIN: 'degraded_without_opt_in'\n};\n\n// Decide eligibility for ONE source_health row given the report/aggregation config.\nfunction decideRun(h, cfg) {\n  h = h || {};\n  cfg = cfg || {};\n  const allowDegraded = cfg.allow_degraded_report === true;\n  const allowFixture = cfg.allow_fixture_report === true;\n  const id = str(h.source_run_id);\n  const data_mode = low(h.data_mode) || 'live';\n  const quality_status = low(h.quality_status);\n  const review_status = low(h.review_status);\n  const flags = low(h.quality_flags);\n  const reportEligibleFlag = (h.report_eligible === undefined) ? null : bool(h.report_eligible);\n\n  const reasons = [];\n  let degraded_included = false;\n  let warning = '';\n\n  if (data_mode === 'fixture') { if (allowFixture) warning = 'fixture'; else reasons.push(EXCLUDE.FIXTURE); }\n  if (data_mode === 'manual_test') { if (allowFixture) warning = warning || 'manual_test'; else reasons.push(EXCLUDE.MANUAL_TEST); }\n  if (quality_status === 'quarantined') reasons.push(EXCLUDE.QUARANTINED);\n  // PENDING-MINORITY-001: a run-level `pending_review` FLAG only means SOME records in the run are pending \u2014\n  // WF16 sets report_eligible=false ONLY when the WHOLE run is pending (pending===total), which is enforced\n  // below via NOT_ELIGIBLE. So a minority-pending run must stay eligible here and let the per-record gate\n  // (rowEligible) drop the individual pending rows; otherwise one pending sibling poisons every clean,\n  // report_candidate record from the same source run. Only a run-level review_status=pending excludes the run.\n  if (review_status === 'pending') reasons.push(EXCLUDE.PENDING);\n  if (/semantic_validation_failed/.test(flags)) reasons.push(EXCLUDE.SEMANTIC);\n  if (/(^|; )stale_source/.test(flags)) reasons.push(EXCLUDE.STALE);\n\n  if (quality_status === 'degraded') {\n    if (allowDegraded) { degraded_included = true; warning = warning || 'degraded'; }\n    else reasons.push(EXCLUDE.DEGRADED_NO_OPTIN);\n  }\n\n  // WF16's own report_eligible=false is authoritative, UNLESS an explicit opt-in path (degraded/fixture)\n  // is covering this run. This keeps WF16 the single source of truth while honoring operator overrides.\n  const optedIn = degraded_included || (allowFixture && (data_mode === 'fixture' || data_mode === 'manual_test'));\n  if (reportEligibleFlag === false && !optedIn && reasons.indexOf(EXCLUDE.NOT_ELIGIBLE) < 0) {\n    reasons.push(EXCLUDE.NOT_ELIGIBLE);\n  }\n\n  const eligible = reasons.length === 0;\n  return {\n    source_run_id: id,\n    eligible: eligible,\n    degraded_included: eligible && degraded_included,\n    quality_status: quality_status,\n    data_mode: data_mode,\n    review_status: review_status || (/(^|; )pending_review/.test(flags) ? 'pending' : ''),\n    warning: eligible ? warning : '',\n    reason: reasons.join('; ')\n  };\n}\n\n// Build the eligibility index for a set of source_health rows.\nfunction buildEligibility(healthRows, cfg) {\n  healthRows = Array.isArray(healthRows) ? healthRows.filter(Boolean) : [];\n  const decisions = healthRows.map(function (h) { return decideRun(h, cfg); });\n  const index = {};\n  decisions.forEach(function (d) { if (d.source_run_id) index[d.source_run_id] = d; });\n  const eligible = decisions.filter(function (d) { return d.eligible; });\n  const warnings = eligible.filter(function (d) { return d.warning; })\n    .map(function (d) { return { source_run_id: d.source_run_id, warning: d.warning }; });\n  return {\n    decisions: decisions,\n    index: index,\n    eligible_run_ids: eligible.map(function (d) { return d.source_run_id; }),\n    degraded_run_ids: eligible.filter(function (d) { return d.degraded_included; }).map(function (d) { return d.source_run_id; }),\n    excluded_run_ids: decisions.filter(function (d) { return !d.eligible; }).map(function (d) { return d.source_run_id; }),\n    warnings: warnings,\n    has_warning: warnings.length > 0,\n    has_fixture_warning: warnings.some(function (w) { return w.warning === 'fixture' || w.warning === 'manual_test'; }),\n    has_degraded_included: eligible.some(function (d) { return d.degraded_included; })\n  };\n}\n\n// Per-record gate (Patch 3): merge RECORD-LOCAL lineage with the matching source_health verdict \u2014 the\n// stricter of the two wins \u2014 and FAIL CLOSED in production. A record enters intelligence only when it is\n// affirmatively verified eligible: either source_health matched-and-eligible, or the record self-attests\n// live + healthy + report-eligible. Unverified records (no health match AND no affirmative local lineage,\n// or missing source_run_id) are EXCLUDED by default; including them requires the explicit operator override\n// cfg.allow_unverified_source=true (a non-production setting). This does not rely on a source_health join\n// alone, so it still gates correctly when source_health is empty or the run is unscored.\nfunction rowEligible(row, elig, cfg) {\n  row = row || {};\n  cfg = cfg || {};\n  const allowFixture = cfg.allow_fixture_report === true;\n  const allowDegraded = cfg.allow_degraded_report === true;\n  const allowUnverified = cfg.allow_unverified_source === true; // explicit non-production/operator fail-open\n  // RUN-LINEAGE-001: the row's identity comes from the SHARED resolver, not from one field. WF04's queue rows\n  // carry the family ONLY in run_id, so reading source_run_id alone reported `no_lineage` for a row that had\n  // perfectly good lineage (live: WF10 exec 932). Same defect class as ISO-ARID-001 / ISO-RUNID-001.\n  const __lin = resolveRunLineage(row);\n  const id = __lin.source_run_id || __lin.agent_request_id;\n  const reasons = [];\n  let warning = '';\n\n  // --- source_health verdict (authoritative when the run is matched) ---\n  let healthMatched = false, healthEligible = false, healthDegradedIncluded = false;\n  if (elig && elig.index && id && Object.prototype.hasOwnProperty.call(elig.index, id)) {\n    healthMatched = true;\n    const d = elig.index[id];\n    healthEligible = d.eligible;\n    healthDegradedIncluded = d.degraded_included;\n    if (!d.eligible) reasons.push('run_excluded:' + d.reason);\n    else warning = d.warning || '';\n  }\n\n  // --- record-local lineage (always evaluated; the stricter of local/health wins) ---\n  const dm = low(row.data_mode);\n  const qs = low(row.quality_status);\n  const review = low(row.review_status);\n  const parse = low(row.parse_method);\n  const flags = low(row.quality_flags);\n  const reportFlag = row.report_eligible;\n  const reportFalse = (reportFlag === false || String(reportFlag).toLowerCase() === 'false');\n  if (dm === 'fixture' && !allowFixture) reasons.push(EXCLUDE.FIXTURE);\n  if (dm === 'manual_test' && !allowFixture) reasons.push(EXCLUDE.MANUAL_TEST);\n  if (qs === 'quarantined') reasons.push(EXCLUDE.QUARANTINED);\n  if (qs === 'degraded' && !allowDegraded) reasons.push(EXCLUDE.DEGRADED_NO_OPTIN);\n  if (review === 'pending' || /(^|; )pending_review/.test(flags)) reasons.push(EXCLUDE.PENDING);\n  if (parse === 'deterministic_uncertain_no_llm') reasons.push('parse_method=deterministic_uncertain_no_llm');\n  if (reportFalse) reasons.push(EXCLUDE.NOT_ELIGIBLE);\n  if (/(^|; )stale_source/.test(flags)) reasons.push(EXCLUDE.STALE);\n\n  // --- verification gate: production requires affirmative eligibility evidence (fail closed) ---\n  // PARSE-OUTCOME-001 (operator decision): a SINGLE successful bounded repair whose payload passed full local\n  // validation is `accepted_with_repair` \u2014 audited and confidence-capped, but reportable exactly like healthy.\n  // A failed repair / fallback-only / invalid result still lands in degraded|quarantined and stays fail-closed.\n  const selfAttestsLive = (dm === 'live' || dm === 'production') &&\n    (qs === '' || qs === 'healthy' || qs === 'accepted_with_repair' || (qs === 'degraded' && allowDegraded)) && !reportFalse;\n  // explicit operator opt-in to a fixture/manual report verifies that data too (it is watermarked downstream)\n  const fixtureOptedIn = allowFixture && (dm === 'fixture' || dm === 'manual_test');\n  const verified = (healthMatched && healthEligible) || (selfAttestsLive && cfg.require_source_health !== true) || fixtureOptedIn;\n  if (reasons.length === 0 && !verified && !allowUnverified) {\n    reasons.push(healthMatched ? 'unverified' : (id ? 'no_source_health' : 'no_lineage'));\n  }\n\n  const eligible = reasons.length === 0;\n  const degraded_included = eligible && (healthDegradedIncluded || (qs === 'degraded' && allowDegraded));\n  return { eligible: eligible, reason: reasons.join('; '), warning: eligible ? warning : '', degraded_included: degraded_included };\n}\n\n// WF12 trend baseline: pick the most recent PRIOR run that is comparable to the current snapshot.\n// Compatible = same niche/region/service filters, live/compatible data_mode, report eligible, non-empty,\n// and within a comparable time window. When none qualifies -> trend_status=no_compatible_baseline and\n// items must NOT all be marked NEW.\nfunction selectBaseline(candidates, current, cfg) {\n  candidates = Array.isArray(candidates) ? candidates.filter(Boolean) : [];\n  current = current || {};\n  cfg = cfg || {};\n  const cn = low(current.niche_id), cr = low(current.region), cs = low(current.service_filter);\n  const curWin = Number(current.time_window_days) || Number(cfg.time_window_days) || 30;\n  const compat = candidates.filter(function (b) {\n    if (str(b.run_stamp) === str(current.run_stamp)) return false;     // not the current run\n    if (cn && low(b.niche_id) && low(b.niche_id) !== cn) return false;\n    if (cr && low(b.region) && low(b.region) !== cr) return false;\n    if (cs && low(b.service_filter) && low(b.service_filter) !== cs) return false;\n    if (b.data_mode && ['live', 'production'].indexOf(low(b.data_mode)) < 0) return false; // fixture/manual baselines excluded\n    if (b.report_eligible === false) return false;\n    if (Number(b.rows_after_filters) <= 0) return false;               // empty snapshot is not a baseline\n    if (b.time_window_days && Number(b.time_window_days) !== curWin) return false; // comparable window\n    return true;\n  });\n  compat.sort(function (a, b) { return str(a.run_stamp) < str(b.run_stamp) ? -1 : 1; });\n  const baseline = compat.length ? compat[compat.length - 1] : null;\n  return {\n    baseline: baseline,\n    trend_status: baseline ? 'baseline_selected' : 'no_compatible_baseline',\n    baseline_run_stamp: baseline ? str(baseline.run_stamp) : ''\n  };\n}\n\n// Render a trend marker for a frequency vs an optional previous frequency. NEVER produce a dangling\n// \"(x34 =)\" \u2014 when there is no compatible baseline the marker is empty; an unchanged value renders no\n// bare \"=\" suffix. (Closure C1-D1 / S3-D7.)\nfunction trendMarker(freq, prevFreq, hasBaseline) {\n  freq = Number(freq) || 0;\n  if (!hasBaseline) return '';            // no baseline -> no suffix at all (not \"NEW\", not \"=\")\n  if (prevFreq === undefined || prevFreq === null) return 'new';\n  if (freq > prevFreq) return 'up';\n  if (freq < prevFreq) return 'down';\n  return 'flat';                          // unchanged -> a word, never a bare \"=\"\n}\n\n// Compose the \"(xN)\" / \"(xN, up)\" suffix safely. Empty/whitespace markers never leave a dangling token.\nfunction freqSuffix(freq, marker) {\n  freq = Number(freq) || 0;\n  const m = str(marker);\n  return m ? ('(x' + freq + ', ' + m + ')') : ('(x' + freq + ')');\n}\n// --- end embedded report_gate ---\n// WF10 v0.3 \u2014 DETERMINISTIC market intelligence aggregation (DEC-104; v0.2 patch DEC-106/107; v0.3 patch DEC-127). No LLM, no external calls, $0.\n// v0.2: no-data guard (rows_after_filters=0 -> marked no_data plan, no normal outputs); entity resolution\n// prefers company_name -> profile_url -> canonical listing id from source_url -> profile_name -> offer+platform.\n// WF08 answers \"what is this one record?\"; WF10 answers \"what is happening in the market overall?\".\n// Aggregate-only audience analysis (CONTACT_AND_OUTREACH_POLICY): no contacts invented, no outreach recommendations.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\nfunction cut(s,n){s=str(s);if(!(s.length>n)||!(n>0))return s;var head=s.slice(0,Math.max(1,n-1));var c2=head.replace(/\\s+\\S*$/,'')||head;return c2.replace(/[\\s,;:.\\u2013\\u2014-]+$/,'')+'\\u2026';}\nfunction hashText(s){s=str(s);let h=5381;for(let i=0;i<s.length;i++){h=((h<<5)+h+s.charCodeAt(i))>>>0;}return ('00000000'+h.toString(16)).slice(-8);}\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction uniqPush(arr,v,max){v=str(v);if(!v)return;if(arr.indexOf(v)<0&&arr.length<(max||9999))arr.push(v);}\nconst cfg=$('Set Aggregator Config').first().json;\nconst now=moscowIsoNow();\nconst stamp=str(cfg.run_id).replace(/^wf10_/,'');\nconst windowDays=Number(cfg.time_window_days)||30;\nconst cutoffMs=Date.now()-windowDays*86400000;\nconst maxEx=Number(cfg.max_examples_per_item)||3;\nconst pf=low(cfg.platform_filter);\nconst stf=low(cfg.service_type_filter);\nconst rf=low(cfg.region_filter);\nfunction readTab(node,tab){let out=[];try{out=$(node).all().map(i=>i.json).filter(Boolean);}catch(e){out=[];}\n  return out.filter(r=>Object.keys(r).some(k=>str(r[k])!=='' )).map(r=>Object.assign({_tab:tab},r));}\nlet rows=[].concat(readTab('Read monitor_queue','monitor_queue'),readTab('Read content_queue','content_queue'),readTab('Read review_queue','review_queue'));\nconst rows_considered=rows.length;\nfunction inWindow(r){const t=Date.parse(str(r.created_at)||str(r.parsed_at));if(isNaN(t))return false;return t>=cutoffMs;}\nrows=rows.filter(inWindow);\nconst rows_in_window=rows.length;\nfunction matches(r){\n  // EXPLICIT-SCOPE-001: 'ANY' disables an INFERRED filter. When the user names a source outright\n  // (\"\u043e\u0442\u0447\u0451\u0442 \u043f\u043e autolombardn1.ru\"), the named source IS the scope \u2014 an inferred region/service default must not\n  // exclude it. Live: the site declared region \"\u0420\u043e\u0441\u0441\u0438\u044f\" while the plan defaulted to \"\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e\", so the region\n  // filter dropped the very source the user asked about (rows_after_isolation=0, empty report). Run isolation\n  // already scopes these rows to this request, so the extra inferred filter only ever subtracts.\n  if(pf&&low(pf)!=='any'&&low(r.platform)!==pf)return false;\n  if(stf&&low(stf)!=='any'&&low(r.service_type)!==stf)return false;\n  if(rf&&low(rf)!=='any'){const reg=low(r.region);if(reg&&reg.indexOf(rf)<0&&rf.indexOf(reg)<0)return false;}\n  return true;\n}\nrows=rows.filter(matches);\n// --- Stage C Closure Patch 2: run isolation + WF16 source_health enforcement ---\nconst __healthRows=readTab('Read source_health','source_health');\nconst __gate=buildEligibility(__healthRows, cfg);\nfunction __isoMatch(r){\n  if(str(cfg.source_run_id_filter)&&!runFamilyMatch(str(r.source_run_id)||str(r.agent_request_id)||str(r.run_id),str(cfg.source_run_id_filter)))return false; // SCOPE-STRICT-001 + ISO-RUNID-001: WF04's queue rows carry the request family ONLY in run_id (req_x::source::slot); source_run_id/agent_request_id are empty, and runFamilyMatch('')===false, so isolation dropped every row of its OWN run.\n  if(str(cfg.agent_request_id_filter)&&!runFamilyMatch(str(r.agent_request_id)||str(r.source_run_id)||str(r.run_id),str(cfg.agent_request_id_filter)))return false; // ISO-ARID-001: queue rows carry no agent_request_id column \u2014 isolate by request family via the source_run_id root (req_x::source::slot) instead of a strict equality that drops every row.\n  if(str(cfg.data_mode_filter)&&str(r.data_mode)&&low(r.data_mode)!==low(cfg.data_mode_filter))return false; // ISO-RUNID-001: queue rows leave data_mode empty. ABSENT is not a MISMATCH (same guard the region filter already uses); a real 'fixture' row is still non-empty and still excluded, and run isolation above already scopes to this request.\n  if(str(cfg.report_eligible_filter)!==''&&String(r.report_eligible)!==str(cfg.report_eligible_filter))return false;\n  if(str(cfg.quality_status_filter)&&low(r.quality_status)!==low(cfg.quality_status_filter))return false;\n  if(str(cfg.review_status_filter)&&low(r.review_status)!==low(cfg.review_status_filter))return false;\n  return true;\n}\nrows=rows.filter(__isoMatch);\nconst rows_after_isolation=rows.length;\nlet rows_excluded_by_health=0; let rows_excluded_by_review=0; const __exclReasons={};\nif(cfg.enforce_source_health!==false){\n  rows=rows.filter(function(r){\n    const v=rowEligible(r,__gate,cfg);\n    if(!v.eligible){\n      if(/review|pending|deterministic_uncertain/.test(v.reason))rows_excluded_by_review++; else rows_excluded_by_health++;\n      __exclReasons[v.reason]=(__exclReasons[v.reason]||0)+1; return false;\n    }\n    return true;\n  });\n}\nconst rows_after_filters=rows.length;\n// --- Stage C Closure Patch 3: stamp source lineage onto aggregate outputs so WF12 can gate defensively ---\nfunction __lin(arr){var ids=[],dm={};for(var i=0;i<arr.length;i++){var rr=arr[i]||{};var id=str(rr.source_run_id);if(id&&ids.indexOf(id)<0)ids.push(id);var d=low(rr.data_mode)||'live';dm[d]=1;}var dks=Object.keys(dm);return {source_run_ids:ids.join(', '),data_mode:(dks.length===1?dks[0]:(dks.length===0?'live':'mixed')),report_eligible:true};}\n// ---- shared vocab (hardcoded credit_brokerage v0.1; NICHE_PACK_SYSTEM_PLAN will externalize this) ----\nconst KW=[['\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440','\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','\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','\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\u0449\u044c \u0441 \u043a\u0440\u0435\u0434\u0438\u0442','\u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043a\u0440\u0435\u0434\u0438\u0442\u043e\u043c'],['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432'],['\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\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438'],['\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'],['\u043f\u043e\u0434\u0431\u043e\u0440 \u0431\u0430\u043d\u043a','\u043f\u043e\u0434\u0431\u043e\u0440 \u0431\u0430\u043d\u043a\u0430'],['\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435','\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435 \u043a\u0440\u0435\u0434\u0438\u0442\u0430'],['\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430','\u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442'],['\u043e\u0431\u043e\u0440\u043e\u0442\u043d','\u043e\u0431\u043e\u0440\u043e\u0442\u043d\u044b\u0435 \u043a\u0440\u0435\u0434\u0438\u0442\u044b'],['\u0442\u0435\u043d\u0434\u0435\u0440\u043d','\u0442\u0435\u043d\u0434\u0435\u0440\u043d\u044b\u0435 \u0437\u0430\u0439\u043c\u044b'],['\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a \u0433\u0430\u0440\u0430\u043d\u0442','\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'],['\u0431\u044b\u0441\u0442\u0440\u043e','\u0431\u044b\u0441\u0442\u0440\u043e'],['\u0441\u0440\u043e\u0447\u043d\u043e','\u0441\u0440\u043e\u0447\u043d\u043e']];\nfunction extractKw(blob){const seen={};const out=[];for(const p of KW){if(blob.indexOf(p[0])>=0&&!seen[p[1]]){seen[p[1]]=1;out.push(p[1]);}}return out;}\n// v0.3 (DEC-127): pain labels merged for manager readability; trust pains (prepayment/fraud fear) added \u2014\n// these dominate VK audience comments and were previously invisible in top_pains.\nconst PAINS=[['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a','\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043e\u0442\u043a\u0430\u0437\u0430\u043b\u0438','\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418'],['\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418'],['\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418'],['\u0441\u0440\u043e\u0447\u043d\u043e \u043d\u0443\u0436\u043d\u044b \u0434\u0435\u043d\u044c\u0433\u0438','\u0441\u0440\u043e\u0447\u043d\u0430\u044f \u043f\u043e\u0442\u0440\u0435\u0431\u043d\u043e\u0441\u0442\u044c \u0432 \u0434\u0435\u043d\u044c\u0433\u0430\u0445'],['\u043d\u0435 \u0434\u0430\u044e\u0442 \u043a\u0440\u0435\u0434\u0438\u0442','\u043d\u0435 \u0434\u0430\u044e\u0442 \u043a\u0440\u0435\u0434\u0438\u0442'],['\u0431\u043e\u044e\u0441\u044c \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u0431\u0435\u0440\u0443\u0442 \u0434\u0435\u043d\u044c\u0433\u0438 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u0434\u0435\u043d\u044c\u0433\u0438 \u0437\u0430 \u043a\u0440\u0435\u0434\u0438\u0442 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u043e\u0431\u043c\u0430\u043d','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432']];\nfunction extractPains(blob){const seen={};const out=[];for(const p of PAINS){if(blob.indexOf(p[0])>=0&&!seen[p[1]]){seen[p[1]]=1;out.push(p[1]);}}return out;}\nfunction rowBlob(r){return low(str(r.offer_text)+' '+str(r.terms)+' '+str(r.reason)+' '+str(r.detected_need)+' '+cut(str(r.text_context),900)+' '+cut(str(r.comment_text),900));}\nfunction listingId(u){const m=str(u).match(/_(\\d{6,})(?:[\\/?#]|$)/)||str(u).match(/\\/(\\d{7,})(?:[\\/?#]|$)/);return m?m[1]:'';}\nfunction normName(s){return low(s).replace(/[^a-z\u0430-\u044f\u04510-9]+/gi,' ').replace(/\\s+/g,' ').trim();}\nfunction adChannel(r){const p=low(r.platform);if(p==='avito'||low(r.source_type)==='classified')return 'classifieds';if(p)return p;return 'unknown';}\n// ---- 1. competitor_profiles (group competitor records) ----\nconst compRows=rows.filter(r=>low(r.entity_type)==='competitor'||r._tab==='monitor_queue');\nfunction normProfileUrl(u){u=low(u);if(!u)return '';u=u.replace(/[?#].*$/,'').replace(/^https?:\\/\\//,'').replace(/^www\\./,'').replace(/\\/+$/,'');return u;}\n// v0.2 entity resolution (DEC-107): named identity first, then stable URL identities (profile_url, canonical\n// Avito/classified listing id from source_url), then profile_name, and normalized offer_text+platform ONLY as\n// a last fallback before the record hash \u2014 so repeated unnamed rows of the same listing form ONE profile.\nfunction groupKey(r){\n  const cn=normName(r.company_name);if(cn)return 'name::'+cn;\n  const pu=normProfileUrl(r.profile_url);if(pu)return 'purl::'+pu;\n  const lid=listingId(r.source_url);if(lid)return 'listing::'+low(r.platform)+'::'+lid;\n  const pn=normName(r.profile_name);if(pn)return 'profile::'+pn;\n  const ot=normName(cut(r.offer_text,80));if(ot)return 'offer::'+low(r.platform)+'::'+ot;\n  return 'rec::'+hashText(low(str(r.source_url)+str(r.offer_text)));\n}\nconst groups={};\nfor(const r of compRows){const k=groupKey(r);(groups[k]=groups[k]||[]).push(r);}\nconst competitor_profiles=[];\nfor(const k of Object.keys(groups)){\n  const g=groups[k];\n  const dates=g.map(r=>Date.parse(str(r.created_at))).filter(t=>!isNaN(t)).sort((a,b)=>a-b);\n  function iso(t){var m=new Date(t+10800000);return m.toISOString().replace('Z','+03:00');}\n  const platforms=[],urls=[],services=[],offers=[],terms=[],channels=[];let kws=[],pains=[];let strengthSum=0,strengthN=0;\n  for(const r of g){\n    uniqPush(platforms,low(r.platform));uniqPush(urls,r.source_url,maxEx);uniqPush(services,low(r.service_type));\n    uniqPush(offers,cut(r.offer_text,140),maxEx);uniqPush(terms,cut(r.terms,140),maxEx);uniqPush(channels,adChannel(r));\n    const blob=rowBlob(r);for(const w of extractKw(blob))uniqPush(kws,w);for(const w of extractPains(blob))uniqPush(pains,w);\n    const cs=Number(r.competitor_strength);if(cs>1){strengthSum+=cs;strengthN++;}\n  }\n  const name=str(g[0].company_name)||str(g[0].profile_name)||('(unnamed) '+cut(str(g[0].offer_text),60));\n  const evid=g.length;\n  const hasPriced=g.some(r=>str(r.terms)!=='');\n  const conf=hasPriced?80:(evid>=2?60:45);\n  const avgStrength=strengthN?Math.round(strengthSum/strengthN):0;\n  const strengths=[];\n  if(hasPriced)strengths.push('\u044f\u0432\u043d\u044b\u0439 \u0446\u0435\u043d\u043e\u0432\u043e\u0439 \u044f\u043a\u043e\u0440\u044c ('+terms.join(' / ')+')');\n  if(kws.indexOf('\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442')>=0)strengths.push('\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442');\n  if(kws.indexOf('\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b')>=0)strengths.push('\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b');\n  if(avgStrength>=75)strengths.push('\u0441\u0438\u043b\u044c\u043d\u043e\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435 (\u0441\u0440\u0435\u0434\u043d\u044f\u044f \u043e\u0446\u0435\u043d\u043a\u0430 \u0437\u0430\u043c\u0435\u0442\u043d\u043e\u0441\u0442\u0438 '+avgStrength+')');\n  const weaknesses=[];\n  if(!hasPriced)weaknesses.push('\u0446\u0435\u043d\u0430/\u0443\u0441\u043b\u043e\u0432\u0438\u044f \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b');\n  if(platforms.length===1)weaknesses.push('\u0432\u0438\u0434\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043a\u0430\u043d\u0430\u043b ('+platforms.join('')+')');\n  var __lc=__lin(g);\n  competitor_profiles.push({\n    competitor_id:'comp_'+hashText(k),\n    source_run_ids:__lc.source_run_ids, data_mode:__lc.data_mode, report_eligible:__lc.report_eligible,\n    competitor_name:name,\n    first_seen_at:dates.length?iso(dates[0]):'',\n    last_seen_at:dates.length?iso(dates[dates.length-1]):'',\n    platforms:platforms.join(', '),\n    source_urls:urls.join(' | '),\n    service_types:services.join(', '),\n    offers:offers.join(' | '),\n    prices_terms:terms.join(' | '),\n    semantic_keywords:kws.slice(0,12).join(', '),\n    pain_points_targeted:pains.join(', '),\n    ad_channels:channels.join(', '),\n    observed_strengths:strengths.join('; '),\n    observed_weaknesses:weaknesses.join('; '),\n    evidence_count:evid,\n    source_confidence_score:conf,\n    notes:'wf10 v0.3 deterministic; run '+str(cfg.run_id)+'; window '+windowDays+'d; group_key='+cut(k,60)\n  });\n}\n// ---- 2. market_angles ----\nconst ANGLES=[\n ['speed','\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c / \u0441\u0440\u043e\u0447\u043d\u043e\u0441\u0442\u044c (\u00ab\u0431\u044b\u0441\u0442\u0440\u043e\u00bb, \u00ab\u0437\u0430 1 \u0434\u0435\u043d\u044c\u00bb)','speed',['\u0431\u044b\u0441\u0442\u0440\u043e','\u0441\u0440\u043e\u0447\u043d\u043e','\u0437\u0430 1 \u0434\u0435\u043d\u044c','\u0432 \u0434\u0435\u043d\u044c \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u044f'],'\u043a\u043b\u0438\u0435\u043d\u0442\u0443 \u043d\u0443\u0436\u043d\u043e \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0435\u043c\u0435\u0434\u043b\u0435\u043d\u043d\u043e','\u041f\u043e\u0441\u0442 \u00ab\u0420\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u0441\u0440\u043e\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u0430: \u043e\u0442 \u0447\u0435\u0433\u043e \u043e\u043d\u0438 \u0437\u0430\u0432\u0438\u0441\u044f\u0442\u00bb + \u0447\u0435\u0441\u0442\u043d\u044b\u0439 \u0440\u0430\u0437\u0431\u043e\u0440 \u044d\u0442\u0430\u043f\u043e\u0432'],\n ['price_anchor','\u0446\u0435\u043d\u043e\u0432\u043e\u0439 \u044f\u043a\u043e\u0440\u044c (\u00ab\u043e\u0442 N \u20bd\u00bb)','price',['__PRICE__'],'\u0441\u0442\u0440\u0430\u0445 \u0441\u043a\u0440\u044b\u0442\u044b\u0445 \u043a\u043e\u043c\u0438\u0441\u0441\u0438\u0439','\u041f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0446\u0435\u043d + \u043f\u043e\u0441\u0442 \u00ab\u0418\u0437 \u0447\u0435\u0433\u043e \u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u043c\u0438\u0441\u0441\u0438\u044f \u0431\u0440\u043e\u043a\u0435\u0440\u0430\u00bb'],\n ['no_prepayment','\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','trust',['\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b'],'\u0441\u0442\u0440\u0430\u0445 \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u0447\u0435\u0441\u0442\u0432\u0430/\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','\u041f\u043e\u0441\u0442 \u00ab\u041f\u043e\u0447\u0435\u043c\u0443 \u0447\u0435\u0441\u0442\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b\u00bb + \u0434\u043e\u0433\u043e\u0432\u043e\u0440-\u043e\u0444\u0435\u0440\u0442\u0430'],\n ['result_payment','\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','trust',['\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','\u043f\u043b\u0430\u0442\u0438\u0442\u0435 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','\u043e\u043f\u043b\u0430\u0442\u0430 \u043f\u043e \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0443'],'\u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0438\u0435 \u043a \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u044f\u043c','\u041a\u0435\u0439\u0441 \u0441 \u044d\u0442\u0430\u043f\u0430\u043c\u0438 \u0441\u0434\u0435\u043b\u043a\u0438 \u0438 \u043e\u043f\u043b\u0430\u0442\u043e\u0439 \u043f\u043e \u0444\u0430\u043a\u0442\u0443 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f'],\n ['after_refusals','\u043a\u0440\u0435\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','pain',['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a','\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432'],'\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432','\u0427\u0435\u043a-\u043b\u0438\u0441\u0442 \u00ab7 \u043f\u0440\u0438\u0447\u0438\u043d \u043e\u0442\u043a\u0430\u0437\u0430 \u0431\u0430\u043d\u043a\u0430 \u0438 \u0447\u0442\u043e \u0441 \u043d\u0438\u043c\u0438 \u0434\u0435\u043b\u0430\u0442\u044c\u00bb + \u043a\u0435\u0439\u0441 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432'],\n ['bad_credit_history','\u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418 / \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438','pain',['\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','\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438'],'\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f','\u0413\u0430\u0439\u0434 \u00ab\u041a\u0430\u043a \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0443\u044e \u0438\u0441\u0442\u043e\u0440\u0438\u044e\u00bb + FAQ \u043f\u043e \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0430\u043c'],\n ['business_finance','\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430 (\u0418\u041f/\u041e\u041e\u041e)','segment',['\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\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442'],'\u043a\u0430\u0441\u0441\u043e\u0432\u044b\u0439 \u0440\u0430\u0437\u0440\u044b\u0432 / \u0442\u0435\u043d\u0434\u0435\u0440\u044b','\u0421\u0435\u0440\u0438\u044f \u043f\u043e\u0441\u0442\u043e\u0432 \u0434\u043b\u044f \u0418\u041f/\u041e\u041e\u041e: \u043e\u0431\u043e\u0440\u043e\u0442\u043d\u044b\u0435 \u043a\u0440\u0435\u0434\u0438\u0442\u044b, \u0442\u0435\u043d\u0434\u0435\u0440\u043d\u044b\u0435 \u0437\u0430\u0439\u043c\u044b, \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438'],\n ['mortgage_refinance','\u0438\u043f\u043e\u0442\u0435\u043a\u0430 / \u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435','segment',['\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'],'\u0432\u044b\u0441\u043e\u043a\u0430\u044f \u0441\u0442\u0430\u0432\u043a\u0430 \u043f\u043e \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u043c\u0443 \u043a\u0440\u0435\u0434\u0438\u0442\u0443','\u041a\u0430\u043b\u044c\u043a\u0443\u043b\u044f\u0442\u043e\u0440 \u0432\u044b\u0433\u043e\u0434\u044b \u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f + \u0440\u0430\u0437\u0431\u043e\u0440 \u00ab\u043a\u043e\u0433\u0434\u0430 \u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0435\u0432\u044b\u0433\u043e\u0434\u043d\u043e\u00bb'],\n ['guarantees','\u0440\u0430\u0431\u043e\u0442\u0430 \u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443 / \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438','trust',['\u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443','\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a \u0433\u0430\u0440\u0430\u043d\u0442','\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e'],'\u0441\u0442\u0440\u0430\u0445 \u043d\u0435\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b','\u041f\u043e\u0441\u0442 \u00ab\u0427\u0442\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0432 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0435 \u0441 \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u043c \u0431\u0440\u043e\u043a\u0435\u0440\u043e\u043c\u00bb']\n];\nconst market_angles=[];\nfor(const a of ANGLES){\n  const hits=[];\n  for(const r of rows){\n    const blob=rowBlob(r);\n    const m=(a[3][0]==='__PRICE__')?(/\u043e\u0442\\s*\\d/.test(blob)||/\\d[\\d\\s]*\u20bd/.test(blob)):a[3].some(t=>blob.indexOf(t)>=0);\n    if(m)hits.push(r);\n  }\n  if(!hits.length)continue;\n  const ex=[];for(const r of hits)uniqPush(ex,r.source_url,maxEx);\n  var __la=__lin(hits);\n  market_angles.push({\n    angle_id:'angle_'+a[0]+'_'+stamp,\n    source_run_ids:__la.source_run_ids, data_mode:__la.data_mode, report_eligible:__la.report_eligible,\n    angle_text:a[1],\n    category:a[2],\n    platforms:(function(){const p=[];for(const r of hits)uniqPush(p,low(r.platform));return p.join(', ');})(),\n    frequency:hits.length,\n    example_sources:ex.join(' | '),\n    related_pain:a[4],\n    recommended_content_response:a[5],\n    confidence:hits.length>=3?'high':(hits.length===2?'medium':'low')\n  });\n}\nmarket_angles.sort((x,y)=>y.frequency-x.frequency);\n// ---- 3. audience_activity_signals (AGGREGATE ONLY \u2014 no author identities, no contacts) ----\nconst audience_activity_signals=[];\nconst byPlatform={};\nfor(const r of rows){const p=low(r.platform)||'unknown';(byPlatform[p]=byPlatform[p]||[]).push(r);}\nfor(const p of Object.keys(byPlatform)){\n  const g=byPlatform[p];\n  const topics=[],pains=[];let buying=0,complaints=0,questions=0,objections=0;\n  // v0.3 objection vocabulary (DEC-127): audience distrust language. Counted ONLY on review_queue rows\n  // (audience voice) so competitor ad copy (\"\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b\", \"\u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443\") never inflates objections.\n  const OBJ=['\u0431\u043e\u044e\u0441\u044c \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','\u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442','\u0431\u0435\u0440\u0443\u0442 \u0434\u0435\u043d\u044c\u0433\u0438 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u0434\u0435\u043d\u044c\u0433\u0438 \u0437\u0430 \u043a\u0440\u0435\u0434\u0438\u0442 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u043e\u0431\u043c\u0430\u043d','\u043a\u0438\u0434\u0430\u043b','\u0440\u0430\u0437\u0432\u043e\u0434','\u043d\u0435 \u0432\u0435\u0440\u044e','\u0430 \u0433\u0430\u0440\u0430\u043d\u0442','\u043a\u0430\u043a\u0438\u0435 \u0433\u0430\u0440\u0430\u043d\u0442','\u0435\u0441\u0442\u044c \u043b\u0438 \u0433\u0430\u0440\u0430\u043d\u0442','\u0431\u0435\u0437 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430','\u043d\u0435\u0442 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430'];\n  for(const r of g){\n    uniqPush(topics,low(r.service_type));\n    const blob=rowBlob(r);\n    for(const w of extractPains(blob))uniqPush(pains,w);\n    // AUD-COUNT-001: audience signals (questions/objections/complaints/buying) count ONLY genuine audience-voice\n    // rows \u2014 public comments/discussions (source_type=public_discussion / social_comment, touchpoint=public_comment).\n    // Competitor wall posts + market posts (vk_community_wall / social_channel / scraped_web / classified) and\n    // unknown posts must NEVER be counted as audience questions (the '\u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432 N' inflation).\n    var __isAud=(['public_discussion','social_comment'].indexOf(low(r.source_type))>=0)||low(r.touchpoint_type)==='public_comment';\n    if(__isAud){\n      if(Number(r.lead_signal_score)>=50)buying++;\n      if(blob.indexOf('\u0436\u0430\u043b\u043e\u0431')>=0||blob.indexOf('\u043e\u0431\u043c\u0430\u043d')>=0||blob.indexOf('\u0440\u0430\u0437\u0432\u043e\u0434')>=0)complaints++;\n      if(blob.indexOf('\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439\u0442\u0435')>=0||blob.indexOf('\u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0438\u0442\u0435')>=0||blob.indexOf('?')>=0)questions++;\n      if(OBJ.some(t=>blob.indexOf(t)>=0))objections++;\n    }\n  }\n  var __ls=__lin(g);\n  audience_activity_signals.push({\n    signal_id:'sig_'+p+'_'+stamp,\n    source_run_ids:__ls.source_run_ids, data_mode:__ls.data_mode, report_eligible:__ls.report_eligible,\n    platform:p,\n    source_url:'',\n    time_window:windowDays+'d',\n    active_authors_count:'',\n    repeat_authors_count:'',\n    question_count:questions,\n    objection_count:objections,\n    complaint_count:complaints,\n    buying_intent_count:buying,\n    top_topics:topics.join(', '),\n    top_pains:pains.join(', '),\n    confidence:(objections+questions+buying>0?'medium':'low'),\n    notes:'aggregate-only (CONTACT_AND_OUTREACH_POLICY); classified-listing data carries no audience author info; counts derive from routed business rows'\n  });\n}\n// ---- 4. content_positioning_plan (1 row per run) ----\n// v0.2 no-data guard (DEC-106): with 0 rows after filters there is NO market evidence \u2014 never emit a\n// generic plan (no template lead_magnets/posts). A clearly marked no_data plan row is written instead.\nconst no_data=(rows_after_filters===0);\nconst topAngles=market_angles.slice(0,3);\nconst allPains=[];for(const r of rows){for(const w of extractPains(rowBlob(r)))uniqPush(allPains,w);}\nconst faq=[];const counters=[];const magnets=[];const posts=[];const ads=[];\nfor(const a of topAngles){\n  posts.push(a.recommended_content_response);\n  ads.push('\u041e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0442\u0438\u0432 \u0443\u0433\u043b\u0430 \u00ab'+a.angle_text+'\u00bb: \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u0438\u043a\u0430 \u0432\u043c\u0435\u0441\u0442\u043e \u043e\u0431\u0435\u0449\u0430\u043d\u0438\u0439 ('+a.related_pain+')');\n  counters.push('\u041a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u044b \u0434\u0430\u0432\u044f\u0442 \u043d\u0430 \u00ab'+a.angle_text+'\u00bb \u2014 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c\u044b\u043c\u0438 \u0444\u0430\u043a\u0442\u0430\u043c\u0438 \u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u044f\u043c\u0438 \u0432 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0435');\n}\nfor(const pn of allPains){faq.push('FAQ: '+pn+' \u2014 \u0447\u0442\u043e \u0440\u0435\u0430\u043b\u044c\u043d\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c');}\nif(allPains.indexOf('\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432')>=0)magnets.push('\u0447\u0435\u043a-\u043b\u0438\u0441\u0442 \u00ab7 \u043f\u0440\u0438\u0447\u0438\u043d \u043e\u0442\u043a\u0430\u0437\u0430 \u0431\u0430\u043d\u043a\u0430\u00bb');\nif(allPains.indexOf('\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f')>=0)magnets.push('\u0433\u0430\u0439\u0434 \u00ab\u041a\u0430\u043a \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0443\u044e \u0438\u0441\u0442\u043e\u0440\u0438\u044e\u00bb');\nif(market_angles.some(a=>a.angle_id.indexOf('mortgage')>=0))magnets.push('\u043a\u0430\u043b\u044c\u043a\u0443\u043b\u044f\u0442\u043e\u0440 \u0432\u044b\u0433\u043e\u0434\u044b \u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f');\nif(!magnets.length)magnets.push('\u0447\u0435\u043a-\u043b\u0438\u0441\u0442 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u0434\u043b\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u0430');\nconst evidUrls=[];for(const r of rows)uniqPush(evidUrls,r.source_url,maxEx);\nconst content_positioning_plan=no_data?[{\n  plan_id:'plan_'+stamp+'_no_data',\n  created_at:now,\n  niche:str(cfg.niche_id),\n  region:str(cfg.region_filter),\n  top_angles:'',\n  recommended_posts:'',\n  recommended_ads:'',\n  faq_topics:'',\n  counterarguments:'',\n  lead_magnets:'',\n  source_evidence:'rows=0',\n  next_action:'no_data; broaden filters or source scope'\n}]:[{\n  plan_id:'plan_'+stamp,\n  created_at:now,\n  niche:str(cfg.niche_id),\n  region:str(cfg.region_filter),\n  top_angles:topAngles.map(a=>a.angle_text+' (x'+a.frequency+')').join(' | '),\n  recommended_posts:posts.join(' | '),\n  recommended_ads:ads.join(' | '),\n  faq_topics:faq.join(' | '),\n  counterarguments:counters.join(' | '),\n  lead_magnets:magnets.join(' | '),\n  source_evidence:'rows='+rows_after_filters+' (window '+windowDays+'d); examples: '+evidUrls.join(' | '),\n  next_action:'operator review; use for content plan; no outreach actions (DEC-098)'\n}];\n// ---- 5. source_confidence_rules (seed only if tab is empty) ----\nlet existingRules=[];\ntry{existingRules=$('Read source_confidence_rules').all().map(i=>i.json).filter(r=>r&&str(r.rule_id)!=='');}catch(e){existingRules=[];}\nconst seedWanted=(cfg.write_confidence_seed_rules_if_empty===true||low(cfg.write_confidence_seed_rules_if_empty)==='true');\nlet source_confidence_rules=[];\nif(seedWanted&&existingRules.length===0){\n  source_confidence_rules=[\n    {rule_id:'sc_rule_01',pattern:'direct competitor listing with price/offer',confidence_effect:'high (75-85)',rationale:'first-party ad with concrete terms',created_at:now},\n    {rule_id:'sc_rule_02',pattern:'competitor website page',confidence_effect:'high (75-85)',rationale:'first-party positioning',created_at:now},\n    {rule_id:'sc_rule_03',pattern:'same pain repeated across >=3 independent sources',confidence_effect:'high (75-85)',rationale:'cross-source confirmation',created_at:now},\n    {rule_id:'sc_rule_04',pattern:'one isolated comment',confidence_effect:'low/medium (30-55)',rationale:'single anecdote',created_at:now},\n    {rule_id:'sc_rule_05',pattern:'vague source_candidate',confidence_effect:'low (20-40)',rationale:'unverified origin',created_at:now},\n    {rule_id:'sc_rule_06',pattern:'duplicate/reposted content',confidence_effect:'lower (count once, discount repeats)',rationale:'inflated frequency',created_at:now},\n    {rule_id:'sc_rule_07',pattern:'explicit public contact present',confidence_effect:'raises lead confidence only',rationale:'still requires CONTACT_AND_OUTREACH_POLICY checks',created_at:now}\n  ];\n}\n// --- Stage C Closure Patch 2: separate OBSERVED counters from INFERRED (no-LLM) insights (S3-D6) ---\nconst observed_audience_signals=audience_activity_signals.map(function(s){return {signal_id:s.signal_id,platform:s.platform,time_window:s.time_window,question_count:s.question_count,objection_count:s.objection_count,complaint_count:s.complaint_count,buying_intent_count:s.buying_intent_count,basis:'observed'};});\nconst inferred_audience_insights=audience_activity_signals.map(function(s){return {platform:s.platform,inferred_pains:s.top_pains,hypothesized_content_themes:s.top_topics,likely_objections:'',basis:'inferred_deterministic_no_llm'};});\nconst platformsSeen=[];for(const r of rows)uniqPush(platformsSeen,low(r.platform)||'unknown');\nconst tabsSeen=[];for(const r of rows)uniqPush(tabsSeen,r._tab);\nreturn [{ json: {\n  competitor_profiles:competitor_profiles,\n  market_angles:market_angles,\n  audience_activity_signals:audience_activity_signals,\n  observed_audience_signals:observed_audience_signals,\n  inferred_audience_insights:inferred_audience_insights,\n  content_positioning_plan:content_positioning_plan,\n  source_confidence_rules:source_confidence_rules,\n  stats:{\n    rows_considered:rows_considered,\n    rows_in_window:rows_in_window,\n    rows_after_filters:rows_after_filters,\n    rows_after_isolation:rows_after_isolation,\n    rows_excluded_by_health:rows_excluded_by_health,\n    rows_excluded_by_review:rows_excluded_by_review,\n    source_health_runs_evaluated:__healthRows.length,\n    source_health_gate:{eligible_run_ids:__gate.eligible_run_ids,excluded_run_ids:__gate.excluded_run_ids,degraded_run_ids:__gate.degraded_run_ids,has_degraded_included:__gate.has_degraded_included,has_warning:__gate.has_warning,warnings:__gate.warnings},\n    source_health_excluded_reasons:__exclReasons,\n    competitor_rows:compRows.length,\n    competitor_profiles:competitor_profiles.length,\n    market_angles:market_angles.length,\n    audience_signals:audience_activity_signals.length,\n    plans:content_positioning_plan.length,\n    confidence_rules_written:source_confidence_rules.length,\n    confidence_rules_existing:existingRules.length,\n    time_window_days:windowDays,\n    no_data:no_data,\n    source_mix:(function(){var _dm={},_pl={};for(var _i=0;_i<rows.length;_i++){var _r=rows[_i];_dm[(low(_r.data_mode)||'live')]=1;_pl[(low(_r.platform)||'unknown')]=1;}return no_data?'none':(Object.keys(_dm).sort().join('+')+': '+Object.keys(_pl).sort().join(', '));})(), // \u00a72 derived from rows actually included\n    platforms_seen:platformsSeen.join(', '),\n    tabs_seen:tabsSeen.join(', ')\n  }\n}}];"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000b1",
      "name": "Build competitor_profiles Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1180,
        100
      ],
      "parameters": {
        "jsCode": "// Map aggregated competitor_profiles to sheet rows. No external calls.\nconst a=$('Aggregate Market Intelligence').first().json;\nreturn (a.competitor_profiles||[]).map(r=>({json:r}));"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000a1",
      "name": "Append competitor_profiles",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1400,
        100
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "competitor_profiles",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000b2",
      "name": "Build market_angles Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1180,
        280
      ],
      "parameters": {
        "jsCode": "// Map aggregated market_angles to sheet rows. No external calls.\nconst a=$('Aggregate Market Intelligence').first().json;\nreturn (a.market_angles||[]).map(r=>({json:r}));"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000a2",
      "name": "Append market_angles",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1400,
        280
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "market_angles",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000b3",
      "name": "Build audience_activity_signals Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1180,
        460
      ],
      "parameters": {
        "jsCode": "// Map aggregated audience_activity_signals to sheet rows. No external calls.\nconst a=$('Aggregate Market Intelligence').first().json;\nreturn (a.audience_activity_signals||[]).map(r=>({json:r}));"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000a3",
      "name": "Append audience_activity_signals",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1400,
        460
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "audience_activity_signals",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000b4",
      "name": "Build content_positioning_plan Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1180,
        640
      ],
      "parameters": {
        "jsCode": "// Map aggregated content_positioning_plan to sheet rows. No external calls.\nconst a=$('Aggregate Market Intelligence').first().json;\nreturn (a.content_positioning_plan||[]).map(r=>({json:r}));"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000a4",
      "name": "Append content_positioning_plan",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1400,
        640
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "content_positioning_plan",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000b5",
      "name": "Build source_confidence_rules Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1180,
        820
      ],
      "parameters": {
        "jsCode": "// Map aggregated source_confidence_rules to sheet rows. No external calls.\nconst a=$('Aggregate Market Intelligence').first().json;\nreturn (a.source_confidence_rules||[]).map(r=>({json:r}));"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000a5",
      "name": "Append source_confidence_rules",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1400,
        820
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "source_confidence_rules",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000bq",
      "name": "Build agent_requests Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1180,
        1000
      ],
      "parameters": {
        "jsCode": "// One agent_requests row (21 columns, same schema as WF07/WF09). status=completed. No external calls.\n// v0.2 (DEC-106/108): result_summary says no_data explicitly when rows_after_filters=0; notes carry the\n// source_mix label so reports never imply all aggregated data was collected live in this run.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Aggregator Config').first().json;\nconst s=$('Aggregate Market Intelligence').first().json.stats;\nconst filters='niche='+str(cfg.niche_id)+'; platform='+(str(cfg.platform_filter)||'all')+'; region='+(str(cfg.region_filter)||'all')+'; service_type='+(str(cfg.service_type_filter)||'all')+'; window='+s.time_window_days+'d';\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_text:'Market intelligence aggregation: '+filters,\n  request_type:str(cfg.request_type)||'market_intelligence_aggregation',\n  source_scope:'business_queues',\n  platforms:str(cfg.platform_filter)||'all',\n  query:filters,\n  region:str(cfg.region_filter)||'all',\n  service_focus:str(cfg.niche_id)||'credit_brokerage',\n  requested_limit:Number(cfg.time_window_days)||30,\n  status:'completed',\n  plan_summary:'WF10 v0.3 deterministic aggregation of monitor_queue/content_queue/review_queue into competitor_profiles / market_angles / audience_activity_signals / content_positioning_plan (+ confidence-rule seed). No LLM, no external API, $0.',\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:(s.no_data?'no_data; ':'')+'rows_considered='+s.rows_considered+'; rows_in_window='+s.rows_in_window+'; rows_after_filters='+s.rows_after_filters+'; competitor_profiles='+s.competitor_profiles+'; market_angles='+s.market_angles+'; audience_signals='+s.audience_signals+'; plans='+s.plans+'; confidence_rules_written='+s.confidence_rules_written+'; source_mix='+s.source_mix+'.',\n  next_action:(s.no_data?'no_data; broaden filters or source scope. No outreach actions (DEC-098).':'Review competitor_profiles / market_angles / content_positioning_plan tabs. No outreach actions (DEC-098).'),\n  notes:'WF10 v0.3 deterministic Competitor/Audience Intelligence Aggregator (DEC-104/106/107/108/127). Aggregate-only audience analysis per CONTACT_AND_OUTREACH_POLICY. source_mix=mixed: live + historical/manual + web pipeline \u2014 aggregated rows may come from live connector runs (WF09), manual/fixture intake (WF07) and the historical web pipeline (WF03/04/05/06); not all data was collected live in this run.'\n}}];"
      }
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000aq",
      "name": "Append agent_requests",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1400,
        1000
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "agent_requests",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "aa100000-0000-0000-0000-0000000000fs",
      "name": "Final Summary Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        1000
      ],
      "parameters": {
        "jsCode": "// Final run summary. No external calls.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Aggregator Config').first().json;\nconst s=$('Aggregate Market Intelligence').first().json.stats;\nreturn [{ json: Object.assign({\n  workflow:'10 - Competitor Audience Intelligence Aggregator',\n  agent_request_id:str(cfg.agent_request_id),\n  run_id:str(cfg.run_id),\n  niche_id:str(cfg.niche_id),\n  filters:{platform:str(cfg.platform_filter),region:str(cfg.region_filter),service_type:str(cfg.service_type_filter)},\n  next_action:(s.rows_after_filters>0?'Inspect the 4 intelligence tabs; use content_positioning_plan for the content plan. No outreach actions.':'no_data; broaden filters or source scope: run source connectors (WF09/WF11) + WF08 first, or widen time_window_days/filters.')\n}, s) }];"
      }
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "source_health",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "id": "aa100000-0000-0000-0000-0000000000sh",
      "name": "Read source_health",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        830,
        700
      ],
      "alwaysOutputData": true,
      "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": "platform_filter",
              "type": "string"
            },
            {
              "name": "region_filter",
              "type": "string"
            },
            {
              "name": "service_type_filter",
              "type": "string"
            },
            {
              "name": "report_eligible_filter",
              "type": "string"
            },
            {
              "name": "quality_status_filter",
              "type": "string"
            },
            {
              "name": "review_status_filter",
              "type": "string"
            },
            {
              "name": "time_window_days",
              "type": "string"
            },
            {
              "name": "niche_id",
              "type": "string"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -380,
        720
      ],
      "id": "wf10-subtrig",
      "name": "When Called by Agent"
    }
  ],
  "connections": {
    "Manual Start": {
      "main": [
        [
          {
            "node": "Set Aggregator Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Aggregator Config": {
      "main": [
        [
          {
            "node": "Read monitor_queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read monitor_queue": {
      "main": [
        [
          {
            "node": "Read content_queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read content_queue": {
      "main": [
        [
          {
            "node": "Read review_queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read review_queue": {
      "main": [
        [
          {
            "node": "Read source_confidence_rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read source_confidence_rules": {
      "main": [
        [
          {
            "node": "Read source_health",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Market Intelligence": {
      "main": [
        [
          {
            "node": "Build competitor_profiles Rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build market_angles Rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build audience_activity_signals Rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build content_positioning_plan Rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build source_confidence_rules Rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build agent_requests Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build competitor_profiles Rows": {
      "main": [
        [
          {
            "node": "Append competitor_profiles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build market_angles Rows": {
      "main": [
        [
          {
            "node": "Append market_angles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build audience_activity_signals Rows": {
      "main": [
        [
          {
            "node": "Append audience_activity_signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build content_positioning_plan Rows": {
      "main": [
        [
          {
            "node": "Append content_positioning_plan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build source_confidence_rules Rows": {
      "main": [
        [
          {
            "node": "Append source_confidence_rules",
            "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
          }
        ]
      ]
    },
    "Read source_health": {
      "main": [
        [
          {
            "node": "Aggregate Market Intelligence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Called by Agent": {
      "main": [
        [
          {
            "node": "Set Aggregator Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "aa100000-competitor-audience-intelligence-aggregator-v003-objections-pain-merge-20260612"
}