{
  "name": "12 - Market Intelligence Report Builder (Stakeholder v0.3)",
  "nodes": [
    {
      "id": "cc120000-0000-0000-0000-0000000000n1",
      "name": "Overview Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -360
      ],
      "parameters": {
        "height": 760,
        "width": 640,
        "content": "## 12 \u2014 Market Intelligence Report Builder v0.1 \u2014 \u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u042b\u0419 \u0441\u043a\u0435\u043b\u0435\u0442 (DEC-112/118)\n\n\u041e\u0442\u0447\u0451\u0442\u043d\u044b\u0439 \u0441\u043b\u043e\u0439 \u041d\u0410\u0414 \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u044f\u0434\u0440\u043e\u043c WF10 (DEC-112: Claude \u2014 \u0432 report/control-\u0441\u043b\u043e\u0435, \u041d\u0415 \u0432 WF10).\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):\n1. \u0420\u0443\u0447\u043d\u043e\u0439 \u0441\u0442\u0430\u0440\u0442 \u2192 Set Report Config (`enable_llm_summary=false`, `telegram_send=false`).\n2. \u0427\u0438\u0442\u0430\u0435\u0442 4 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 WF10: competitor_profiles / market_angles / audience_activity_signals / content_positioning_plan.\n3. \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u0442 \u041f\u041e\u0421\u041b\u0415\u0414\u041d\u0418\u0419 \u0441\u043d\u0430\u043f\u0448\u043e\u0442 WF10 \u043f\u043e `plan_id` (\u043e\u0434\u0438\u043d \u043f\u043b\u0430\u043d \u043d\u0430 \u043f\u0440\u043e\u0433\u043e\u043d); \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u043f\u043b\u0430\u043d = \u0431\u0430\u0437\u0430 \u0434\u043b\u044f \u0442\u0440\u0435\u043d\u0434\u043e\u0432 \u0443\u0433\u043b\u043e\u0432 (\u2191/\u2193/=/NEW).\n4. \u0421\u0447\u0438\u0442\u0430\u0435\u0442 \u0442\u043e\u043f \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (evidence_count/confidence), \u0442\u043e\u043f \u0443\u0433\u043b\u043e\u0432 (frequency+\u0442\u0440\u0435\u043d\u0434), \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b \u0430\u0443\u0434\u0438\u0442\u043e\u0440\u0438\u0438; \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u0442 Markdown.\n5. \u041f\u0438\u0448\u0435\u0442 \u0422\u041e\u041b\u042c\u041a\u041e: market_intelligence_reports (25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a v0.3, \u0441\u0445\u0435\u043c\u0430 \u0432 MARKET_INTELLIGENCE_REPORT_SCHEMA.md) + agent_requests (21, completed, $0).\n6. no_data-\u043f\u0440\u043e\u0433\u043e\u043d WF10 \u2192 report_type=no_data_notice, \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u044f, \u0431\u0435\u0437 \u0448\u0430\u0431\u043b\u043e\u043d\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430.\n7. **source_mix \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d \u0432 \u043a\u0430\u0436\u0434\u043e\u043c \u043e\u0442\u0447\u0451\u0442\u0435** (DEC-108): \u043e\u0442\u0447\u0451\u0442 \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u043f\u043e\u0434\u0440\u0430\u0437\u0443\u043c\u0435\u0432\u0430\u0435\u0442, \u0447\u0442\u043e \u0432\u0441\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u043e\u0431\u0440\u0430\u043d\u044b live.\n\n\u0427\u0415\u0413\u041e \u041d\u0415 \u0414\u0415\u041b\u0410\u0415\u0422: \u041d\u0415 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 Claude (\u0432\u0435\u0442\u043a\u0430 = guard \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439; \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0435 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435: bounded facts-only prompt, \u0442\u043e\u043a\u0435\u043d-\u043a\u044d\u043f, \u0437\u0430\u043f\u0438\u0441\u044c \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438); \u041d\u0415 \u0448\u043b\u0451\u0442 \u0432 Telegram (delivered_to=none); \u041d\u0415 \u0445\u043e\u0434\u0438\u0442 \u0432 \u0441\u0435\u0442\u044c (HTTP-\u043d\u043e\u0434 \u043d\u0435\u0442); \u041d\u0415 \u0432\u044b\u0434\u0443\u043c\u044b\u0432\u0430\u0435\u0442 \u0444\u0430\u043a\u0442\u044b/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b; \u041d\u0415 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442 outreach. active=false.\n\n**v0.2 (DEC-122):** \u043e\u0442\u0447\u0451\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d \u0441\u0435\u043a\u0446\u0438\u044f\u043c\u0438 executive_summary / competitor_snapshot / top_offers_and_prices / source_confidence / limitations / next_actions (\u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e, $0). \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u041e\u0425\u0420\u0410\u041d\u042f\u0415\u041c\u0410\u042f Claude-\u0432\u0435\u0442\u043a\u0430: \u0433\u0435\u0439\u0442 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f (token) -> evidence-bound \u043f\u0440\u043e\u043c\u043f\u0442 (\u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043b\u044f \u043e\u0442\u0447\u0451\u0442\u0430) -> \u041e\u0422\u041a\u041b\u042e\u0427\u0401\u041d\u041d\u042b\u0419 HTTP-\u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440 (claude-sonnet-4-6, \u0431\u0435\u0437 \u043a\u043b\u044e\u0447\u0430) -> merge \u0441 \u0443\u0447\u0451\u0442\u043e\u043c \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 ($3/$15 \u0437\u0430 MTok). WF10 = \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0444\u0430\u043a\u0442\u043e\u0432\u043e\u0435 \u044f\u0434\u0440\u043e; Claude = \u0441\u043b\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u0438\u043d\u0442\u0435\u0440\u043f\u0440\u0435\u0442\u0430\u0446\u0438\u0438 (DEC-112). \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u0432\u044b\u0434\u0443\u043c\u0430\u043d\u043d\u044b\u0445 \u0444\u0430\u043a\u0442\u043e\u0432/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432/\u0430\u0443\u0442\u0440\u0438\u0447\u0430.\n\n**v0.3 (DEC-128/129/130):** 1) \u0434\u0430\u0439\u0434\u0436\u0435\u0441\u0442 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f (5\u20137 \u0447\u0438\u0441\u0442\u044b\u0445 \u043f\u0443\u043d\u043a\u0442\u043e\u0432); 2) \u0447\u0438\u0441\u0442\u044b\u0435 \u0438\u043c\u0435\u043d\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (\u0432\u043c\u0435\u0441\u0442\u043e \u00ab(unnamed) \u2026\u00bb \u2014 \u00ab<Platform> offer: <\u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u043e\u0444\u0444\u0435\u0440>\u00bb); 3) \u043e\u0444\u0444\u0435\u0440\u044b \u0443\u043a\u043e\u0440\u043e\u0447\u0435\u043d\u044b (max_offer_chars=90); 4) \u0431\u043b\u043e\u043a \u0441\u0430\u0439\u0442\u043e\u0432 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0438\u0437 competitor_site_snapshots (Stage 2 reintegration); 5) \u0431\u043b\u043e\u043a \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 \u0438\u0437 public_lead_signals (\u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b, \u0431\u0435\u0437 \u0438\u043c\u0451\u043d/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432); 6) \u0431\u043b\u043e\u043a\u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439: \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 / \u043a\u043e\u043d\u0442\u0435\u043d\u0442 / \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438; 7) \u0441\u0445\u0435\u043c\u0430 \u043e\u0442\u0447\u0451\u0442\u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0430 \u0434\u043e 25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a (llm_status/llm_model/llm_input_tokens/llm_output_tokens/llm_summary_ru/llm_recommendations_ru/llm_quality_flags); 8) Claude-\u0432\u0435\u0442\u043a\u0430: \u0431\u044e\u0434\u0436\u0435\u0442\u043d\u044b\u0439 \u0433\u0435\u0439\u0442 (llm_max_input_chars, llm_max_estimated_cost_usd \u2014 \u0431\u0440\u043e\u0441\u0430\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0414\u041e HTTP), JSON-\u0441\u0435\u043a\u0446\u0438\u0438 (executive_summary_ru/key_findings/market_risks/recommended_next_actions/content_recommendations/source_limitations), quality-\u0444\u043b\u0430\u0433\u0438 (non_json/truncated/unverified_numbers/outreach_language); 9) \u043a\u0430\u0436\u0434\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d \u043f\u0438\u0448\u0435\u0442 1 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 live_source_runs."
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000n2",
      "name": "Setup and Test Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        120,
        -360
      ],
      "parameters": {
        "height": 720,
        "width": 620,
        "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). \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0432\u043a\u043b\u0430\u0434\u043a\u0443 `market_intelligence_reports` (25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a v0.3 \u2014 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0438\u0437 docs/MARKET_INTELLIGENCE_REPORT_SCHEMA.md).\n2. \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 6 sheet-\u043d\u043e\u0434\u0430\u0445 (4 read + 2 append) \u0438 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c PASTE_SPREADSHEET_ID_HERE \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 ID.\n3. \u0422\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0435: \u22651 \u043f\u0440\u043e\u0433\u043e\u043d WF10 (\u0438\u043d\u0430\u0447\u0435 \u043f\u043e\u043d\u044f\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u00abcontent_positioning_plan is empty\u00bb). \u0422\u0440\u0435\u043d\u0434\u044b \u0443\u0433\u043b\u043e\u0432 \u043f\u043e\u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043f\u0440\u0438 \u22652 \u043f\u0440\u043e\u0433\u043e\u043d\u0430\u0445 WF10.\n4. \u0422\u0435\u0441\u0442 1 ($0): Execute once \u2192 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e: market_intelligence_reports +1 (report_type=on_demand; top_competitors/top_angles \u0438\u0437 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0430 WF10; llm_* \u043f\u0443\u0441\u0442\u044b\u0435; llm_cost_usd=0; delivered_to=none; source_mix \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d), agent_requests +1 (request_type=report_summary, completed, $0).\n5. \u0422\u0435\u0441\u0442 2 (no-data): \u0435\u0441\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u043f\u0440\u043e\u0433\u043e\u043d WF10 \u0431\u044b\u043b no_data \u2192 report_type=no_data_notice, \u0442\u043e\u043f-\u0441\u043f\u0438\u0441\u043a\u0438 \u043f\u0443\u0441\u0442\u044b\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u044f \u00abno_data; broaden filters or source scope\u00bb.\n6. \u0422\u0435\u0441\u0442 3 (LLM guard): enable_llm_summary=true \u2192 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043d\u0430 \u043d\u043e\u0434\u0435 LLM Summary Guard. \u0412\u0435\u0440\u043d\u0443\u0442\u044c false.\n\nTelegram-\u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u0438 Claude-summary \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u0448\u0430\u0433\u0438 \u0437\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435\u043c (REPORTING_AND_TELEGRAM_SUMMARY_PLAN.md \u00a75).\n\n**v0.3:** \u0432\u043a\u043b\u0430\u0434\u043a\u0430 market_intelligence_reports \u0442\u0435\u043f\u0435\u0440\u044c 25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a (\u0441\u043c. MARKET_INTELLIGENCE_REPORT_SCHEMA.md v0.3 \u2014 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0442\u044c/\u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438). \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0432\u043a\u043b\u0430\u0434\u043a\u0438 competitor_site_snapshots (22) / public_lead_signals (47, v0.3) / live_source_runs (23) \u0438\u0437 docs/TABLE_SCHEMA.md (\u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c\u0438 \u2014 \u043e\u0442\u0447\u0451\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0431\u0435\u0437 \u043d\u0438\u0445). 8 sheet-\u043d\u043e\u0434 (6 read + 3 append... \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b \u043d\u0430 \u0432\u0441\u0435\u0445). \u0422\u0435\u0441\u0442 Claude (\u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e): llm_approval_token=I_APPROVE_CLAUDE_REPORT_SUMMARY + enable_llm_summary=true + Anthropic \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b \u0432 n8n + \u0412\u041a\u041b\u042e\u0427\u0418\u0422\u042c disabled HTTP-\u043d\u043e\u0434\u0443; \u0431\u044e\u0434\u0436\u0435\u0442-\u0442\u0435\u0441\u0442: llm_max_estimated_cost_usd=0.0001 -> \u043e\u0448\u0438\u0431\u043a\u0430 budget guard \u0414\u041e HTTP."
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000m1",
      "name": "Manual Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -440,
        320
      ],
      "parameters": {}
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000c1",
      "name": "Set Report Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -220,
        320
      ],
      "parameters": {
        "jsCode": "// WF12 Market Intelligence Report Builder config \u2014 v0.3 stakeholder report (DEC-112/118/122/128).\n// $0 by default: no Claude, no Telegram send, no external API. Reads WF10 tabs + competitor_site_snapshots\n// + public_lead_signals; writes ONE market_intelligence_reports row (25 cols, MARKET_INTELLIGENCE_REPORT_SCHEMA.md v0.3),\n// ONE agent_requests row and ONE live_source_runs row.\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction moscowStamp(){var z=function(n){return String(n).padStart(2,'0');};var m=new Date(Date.now()+10800000);return m.getUTCFullYear()+z(m.getUTCMonth()+1)+z(m.getUTCDate())+'_'+z(m.getUTCHours())+z(m.getUTCMinutes())+z(m.getUTCSeconds());}\nconst stamp=moscowStamp();\nvar __cfg = {\n  // --- hard-off defaults (DEC-112): the deterministic report is complete without any of these.\n  enable_llm_summary:false,          // optional Claude summary branch \u2014 guarded, HTTP node DISABLED\n  llm_approval_token:'',             // must equal 'I_APPROVE_CLAUDE_REPORT_SUMMARY' (explicit operator approval)\n  llm_model:'claude-sonnet-4-6',     // project stack model (CLAUDE.md); pricing for cost tracking below\n  llm_max_tokens:1200,               // hard output cap for the summary\n  llm_max_input_chars:8000,          // budget guard (DEC-128): prompt facts hard-capped at this size\n  llm_max_estimated_cost_usd:0.10,   // budget guard (DEC-128): throw BEFORE the HTTP node if estimate exceeds\n  llm_input_price_per_mtok:3.0,      // USD per 1M input tokens (claude-sonnet-4-6)\n  llm_output_price_per_mtok:15.0,    // USD per 1M output tokens (claude-sonnet-4-6)\n  telegram_send:false,               // Telegram delivery \u2014 NOT implemented (delivered_to stays 'none')\n  report_type:'on_demand',           // on_demand / weekly_digest / no_data_notice (auto-set on no_data)\n  niche_id:'credit_brokerage',\n  region:'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n  top_n:3,\n  max_offer_chars:90,                // v0.3: long competitor offers are shortened for readability\n  requested_by:'operator',\n  // --- Stage C Closure Patch 2: WF16 source_health enforcement + explicit fixture-report mode (\u00a711.5/\u00a711.7) ---\n  enforce_source_health:true,\n  require_source_health:true,\n  allow_degraded_report:false,   // explicit opt-in to include degraded runs (renders a visible warning)\n  allow_fixture_report:false,    // explicit opt-in to generate a fixture/manual_test report (renders watermark)\n  report_data_mode:'live',       // live | fixture | manual_test\n  allow_unverified_source:false, // PRODUCTION fail-closed (see WF10); set true ONLY for non-production/dev runs\n  report_id:'report_'+stamp,\n  run_id:'wf12_'+stamp,\n  agent_request_id:'wf12_req_'+stamp,\n  created_at:moscowIsoNow()\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','agent_request_id');\n__ov('report_data_mode','data_mode');\n__ov('report_type','report_type');\n__ov('niche_id','niche_id');\n__ov('region','region');\n__ov('top_n','top_n');\n__ov('enable_llm_summary','enable_llm_summary');\n__ov('llm_approval_token','llm_approval_token');\n__ov('supplied_sources','supplied_sources');\nreturn [{ json: __cfg }];"
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r1",
      "name": "Read competitor_profiles",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        0,
        320
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "competitor_profiles",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r2",
      "name": "Read market_angles",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        220,
        320
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "market_angles",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r3",
      "name": "Read audience_activity_signals",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        440,
        320
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "audience_activity_signals",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r4",
      "name": "Read content_positioning_plan",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        660,
        320
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "content_positioning_plan",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000b1",
      "name": "Build Deterministic Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        320
      ],
      "parameters": {
        "jsCode": "// --- SOCIAL-BRIDGE-001: embedded n8n/lib/social_evidence.js (drift-proof; test asserts equality) ---\nconst __SE = (function () {\n  'use strict';\n// social_evidence.js \u2014 SOCIAL-BRIDGE-001. Turn verbatim `raw_market_records` rows into bounded, strictly-scoped\n// evidence rows that the WF12 report bundle can carry, so a Telegram channel / VK community can be analysed as a\n// SOURCE even when it has no competitor profile.\n//\n// Why this exists (production defect, request req_76722076, executions 1096/1101):\n//   Telegram collection worked \u2014 30 items received, 22 written, 22 relevant, source_outcome=collected_with_data \u2014\n//   but `Build Analysis Inputs` produced ZERO analysis targets, WF28 was never called, records_analyzed was 0, and\n//   the user was told no competitor profiles with offers/prices were found. The verbatim posts were sitting in\n//   raw_market_records the whole time; WF12 simply never read them, and analysis_bridge derives targets from\n//   competitor profiles. Social posts deliberately do NOT become competitor profiles (DEC-133/DEC-135 quality\n//   policy), so valid evidence was lost between collection and analysis.\n//\n// Scoping is FAIL-CLOSED, in this order. A row must survive every gate:\n//   1. request scope   \u2014 the row's agent_request_id/source_run_id must belong to THIS request family;\n//   2. source-run scope\u2014 when the WF16 quality gate produced an eligible-run list, the row's source_run_id must be\n//                        in it (a degraded/excluded run contributes nothing);\n//   3. platform        \u2014 social only (website evidence already flows through competitor_profiles/offers);\n//   4. citable         \u2014 a real public post URL AND non-empty verbatim text, else the model could not cite it;\n//   5. post-level relevance (DEC-133/135) \u2014 decided by the POST TEXT alone: channel/system noise is dropped and\n//                        the text must derive a recognizable service. An unrelated post is never evidence.\n// Bounding: per-source and total caps, and a per-excerpt character cap, so one chatty channel cannot blow up the\n// evidence package or the paid analysis call.\n//\n// Embeddable: unique se*-prefixed names, no cross-lib require. The service classifier is intentionally a verbatim\n// copy of semantic_core.deriveServiceFromText \u2014 a Code node cannot require() a sibling lib, and\n// tests/test_social_evidence.js asserts the two stay identical.\n\nfunction seStr(v) { return v == null ? '' : String(v); }\nfunction seLow(v) { return seStr(v).toLowerCase(); }\nfunction seNum(v, d) { var n = Number(v); return isFinite(n) ? n : (d === undefined ? 0 : d); }\n\n// Same family semantics WF12 already uses for lead rows: \"req_1\" matches \"req_1::telegram::a1\".\nfunction seFamMatch(a, b) {\n  a = seStr(a); b = seStr(b);\n  if (!b) return true;\n  if (!a) return false;\n  return a === b || a.indexOf(b + '::') === 0 || b.indexOf(a + '::') === 0;\n}\n\n// --- verbatim copy of semantic_core.deriveServiceFromText (drift-asserted by the test) -----------------------\nfunction seDeriveService(text) {\n  const t = seLow(text);\n  if (!t) return 'unknown';\n  if (/\u0432\u043e\u0437\u0432\u0440\u0430\u0442\u043d[\u0430-\u044f\u0451]*\\s+\u043b\u0438\u0437\u0438\u043d\u0433|sale.?leaseback|leaseback/.test(t)) return 'auto_lease_refinance';\n  if (/\u043f\u0442\u0441|pts|\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433 \u0430\u0432\u0442\u043e|\u0437\u0430\u043b\u043e\u0433 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b|\u043f\u043e\u0434 \u0430\u0432\u0442\u043e/.test(t)) return 'pts_loan';\n  if (/\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433 \u043d\u0435\u0434\u0432\u0438\u0436|\u0437\u0430\u043b\u043e\u0433 \u043a\u0432\u0430\u0440\u0442\u0438\u0440|\u0437\u0430\u043b\u043e\u0433 \u0434\u043e\u043c\u0430|\u043f\u043e\u0434 \u043a\u0432\u0430\u0440\u0442\u0438\u0440/.test(t)) return 'real_estate_secured_loan';\n  if (/\u0438\u043f\u043e\u0442\u0435\u043a/.test(t) && /\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441/.test(t)) return 'mortgage_refinance';\n  if (/\u0438\u043f\u043e\u0442\u0435\u043a/.test(t)) return 'mortgage_brokerage';\n  if (/\u043a\u0440\u0435\u0434\u0438\u0442\u043d[\u0430-\u044f\u0451]* \u0438\u0441\u0442\u043e\u0440\u0438|\u0438\u0441\u043f\u0440\u0430\u0432[\u0430-\u044f\u0451]* \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043e\u0447\u0438\u0441\u0442[\u0430-\u044f\u0451]* \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u0431\u043a\u0438/.test(t)) return 'credit_history_consulting';\n  if (/\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437|\u0441 \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a|\u043f\u043b\u043e\u0445[\u0430-\u044f\u0451]* \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a/.test(t)) return 'credit_after_refusals';\n  if (/\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430|\u0438\u043f \u0438 \u043e\u043e\u043e|\u043e\u0431\u043e\u0440\u043e\u0442\u043d|\u0442\u0435\u043d\u0434\u0435\u0440\u043d|\u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442/.test(t)) return 'business_credit';\n  if (/\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a[\u0430-\u044f\u0451]* \u0433\u0430\u0440\u0430\u043d\u0442/.test(t)) return 'bank_guarantee';\n  if (/\u0431\u0430\u043d\u043a\u0440\u043e\u0442/.test(t)) return 'bankruptcy_advisory';\n  if (/\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441/.test(t)) return 'debt_refinancing';\n  if (/\u043e\u0431\u044a\u0435\u0434\u0438\u043d[\u0430-\u044f\u0451]* \u043a\u0440\u0435\u0434\u0438\u0442|\u043a\u043e\u043d\u0441\u043e\u043b\u0438\u0434\u0430\u0446/.test(t)) return 'debt_consolidation';\n  if (/\u0437\u0430\u043b\u043e\u0433|\u043f\u043e\u0434 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438/.test(t)) return 'secured_financing';\n  if (/\u043a\u0440\u0435\u0434\u0438\u0442\u043d[\u0430-\u044f\u0451]* \u0431\u0440\u043e\u043a\u0435\u0440|\u043f\u043e\u0434\u0431\u043e\u0440 \u0431\u0430\u043d\u043a|\u043f\u043e\u043c\u043e\u0449\u044c.{0,12}\u043a\u0440\u0435\u0434\u0438\u0442/.test(t)) return 'credit_brokerage';\n  if (/\u043a\u0440\u0435\u0434\u0438\u0442|\u0437\u0430\u0439\u043c/.test(t)) return 'consumer_credit';\n  return 'unknown';\n}\n// --- WIP3-C: niche-primary gate on top of the drift-asserted seDeriveService (semantic_core is NOT modified) ---\n// A post that matched ONLY the broad \u043a\u0440\u0435\u0434\u0438\u0442/\u0437\u0430\u0439\u043c catch-all is admitted as PRIMARY lending evidence only when it is\n// actually about lending \u2014 not macro/bond/sovereign/rating news that merely mentions \u00ab\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0439 \u0440\u0438\u0441\u043a\u00bb/\u00ab\u0434\u043e\u043b\u0433\u00bb.\n// (regression: banksta British Steel / Oracle CDS / generic bond credit-risk must not be primary evidence.)\nvar SE_OFFDOMAIN = /\u043e\u0431\u043b\u0438\u0433\u0430\u0446|\u0435\u0432\u0440\u043e\u0431\u043e\u043d\u0434|\\b\u0431\u043e\u043d\u0434|\\bcds\\b|\u0434\u0435\u0444\u043e\u043b\u0442\u043d[\u0430-\u044f\u0451]* \u0441\u0432\u043e\u043f|\u043a\u0440\u0435\u0434\u0438\u0442\u043d[\u0430-\u044f\u0451]* \u0434\u0435\u0444\u043e\u043b\u0442\u043d|\u0441\u0443\u0432\u0435\u0440\u0435\u043d|\u044d\u043c\u0438\u0442\u0435\u043d\u0442|\\b\u0441\u043f\u0440\u0435\u0434|\u0434\u043e\u0445\u043e\u0434\u043d\u043e\u0441\u0442[\u044c\u0438]|\u043d\u0430\u0446\u0438\u043e\u043d\u0430\u043b\u0438\u0437\u0430\u0446|british steel|oracle|\u0438\u043d\u0434\u0435\u043a\u0441[\u0430-\u044f\u0451]*\\s+(?:\u043c\u043e\u0441\u0431\u0438\u0440\u0436|\u0440\u0442\u0441|s&p|dow)|\u043a\u043e\u0442\u0438\u0440\u043e\u0432\u043a|\u0430\u043a\u0446\u0438[\u0439\u044f\u0435]/;\nvar SE_OFFER_SIGNAL = /\u043e\u0444\u043e\u0440\u043c|\u0437\u0430\u044f\u0432\u043a|\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0442|\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433|\u0432\u044b\u0434\u0430[\u0435\u0451]\u043c|\u0432\u044b\u0434\u0430\u0447|\u043f\u043e\u043b\u0443\u0447\u0438(?:\u0442\u0435|\u0442\u044c)? \u0434\u0435\u043d\u044c\u0433|\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438|\u0434\u0435\u043d\u044c\u0433\u0438 \u0437\u0430|\u0437\u0430\u0439\u043c \u043f\u043e\u0434|\u043a\u0440\u0435\u0434\u0438\u0442 \u043f\u043e\u0434|\u0440\u0430\u0441\u0441\u0440\u043e\u0447\u043a|\u043d\u0430\u0448\u0438 \u0443\u0441\u043b\u043e\u0432\u0438|\u043f\u043e\u0434\u0430\u0442\u044c \u0437\u0430\u044f\u0432\u043a\u0443/;\nfunction seNichePrimary(text, service) {\n  if (service !== 'consumer_credit') return true;           // specific lending services are always primary\n  var t = seLow(text);\n  if (SE_OFFER_SIGNAL.test(t)) return true;                 // a concrete loan-offer signal keeps it primary\n  if (SE_OFFDOMAIN.test(t)) return false;                   // otherwise off-domain macro/bond/rating news \u2192 not primary\n  return true;\n}\n\n// Channel/system noise (\"\u043a\u0430\u043d\u0430\u043b \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\", pinned-message notices \u2026) is never market evidence.\nfunction seIsSystemEvent(text) {\n  var t = seStr(text);\n  return /channel name was changed to/i.test(t)\n    || /(\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435|\u0438\u043c\u044f)\\s+\u043a\u0430\u043d\u0430\u043b\u0430\\s+(\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u043e|\u0441\u043c\u0435\u043d\u0435\u043d\u043e|\u043f\u043e\u043c\u0435\u043d\u044f)/i.test(t)\n    || /(\u0438\u0437\u043c\u0435\u043d\u0438\u043b|\u0441\u043c\u0435\u043d\u0438\u043b|\u043f\u043e\u043c\u0435\u043d\u044f\u043b)\\s+\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\\s+\u043a\u0430\u043d\u0430\u043b\u0430/i.test(t)\n    || /\u043a\u0430\u043d\u0430\u043b\\s+\u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d/i.test(t)\n    || /(\u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0451\u043d\u043d\u043e\u0435|\u0437\u0430\u043a\u0440\u0435\u043f\u043b\u0435\u043d\u043d\u043e\u0435)\\s+\u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435/i.test(t)\n    || /^\\s*(\u0432\u0441\u0442\u0443\u043f\u0438\u043b|\u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0438\u043b)[\u0430-\u044f\u0451]*\\s+\u043a\\s+(\u043a\u0430\u043d\u0430\u043b\u0443|\u0433\u0440\u0443\u043f\u043f\u0435)/i.test(t);\n}\n\nvar SE_SOCIAL_PLATFORMS = ['telegram', 'vk'];\n\nfunction seDecodeEntities(s) {\n  return seStr(s)\n    .replace(/&#(\\d+);/g, function (_, d) { try { return String.fromCharCode(Number(d)); } catch (e) { return ''; } })\n    .replace(/&quot;/g, '\"').replace(/&apos;/g, \"'\").replace(/&nbsp;/g, ' ')\n    .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');\n}\n\n// Contact scrubbing mirrors WF12's report-layer redaction: business numbers survive, direct contact identifiers\n// do not. The excerpt must stay VERBATIM in substance \u2014 we redact identifiers, we never paraphrase.\nvar SE_CONTACT_MARK = '[PUBLIC CONTACT REDACTED]';\nfunction seRedact(s) {\n  s = seStr(s); if (!s) return s;\n  s = s.replace(/[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}/g, SE_CONTACT_MARK);\n  s = s.replace(/(?:\\+7|8)[\\s\\-()]*\\d{3}[\\s\\-()]*\\d{3}[\\s\\-]*\\d{2}[\\s\\-]*\\d{2}/g, SE_CONTACT_MARK);\n  s = s.replace(/\\+\\d[\\d\\s().\\-]{8,}\\d/g, SE_CONTACT_MARK);\n  s = s.replace(/@[A-Za-z0-9_]{3,32}/g, SE_CONTACT_MARK);\n  return s;\n}\n\nfunction seClean(s, max) {\n  var t = seRedact(seDecodeEntities(s)).replace(/\\s+/g, ' ').trim();\n  if (max && t.length > max) t = t.slice(0, max - 1) + '\u2026';\n  return t;\n}\n\n// The logical source a post belongs to: the channel/community, not the individual post.\nfunction seSourceKeyOf(row) {\n  var prof = seStr(row.profile_url) || seStr(row.source_url);\n  var m = prof.match(/^https?:\\/\\/([^\\/?#]+)([^?#]*)/i);\n  if (m) {\n    var host = m[1].replace(/^www\\./i, '');\n    var seg = seStr(m[2]).split('/').filter(function (x) { return x && x !== 's'; })[0] || '';\n    if (seg) return host + '/' + seg;\n    return host;\n  }\n  return seStr(row.author_handle) || seStr(row.profile_name) || seStr(row.platform);\n}\n\n/**\n * buildSocialEvidence(rows, ctx, opts)\n * @param {Array}  rows raw_market_records rows (verbatim collected posts)\n * @param {object} ctx  { agent_request_id, included_source_runs: [] }\n * @param {object} opts { max_total, max_per_source, max_excerpt_chars, platforms }\n * @returns {{evidence: Array, sources: Array, considered: number, dropped: object}}\n *   evidence rows are shaped for bundle.evidence and carry the lineage every downstream consumer needs.\n */\nfunction buildSocialEvidence(rows, ctx, opts) {\n  rows = Array.isArray(rows) ? rows : [];\n  ctx = ctx || {}; opts = opts || {};\n  var maxTotal = seNum(opts.max_total, 40);\n  var maxPerSource = seNum(opts.max_per_source, 12);\n  var maxChars = seNum(opts.max_excerpt_chars, 600);\n  var platforms = Array.isArray(opts.platforms) && opts.platforms.length ? opts.platforms : SE_SOCIAL_PLATFORMS;\n  var reqId = seStr(ctx.agent_request_id);\n  var includedRuns = Array.isArray(ctx.included_source_runs) ? ctx.included_source_runs.filter(Boolean) : [];\n\n  var dropped = { foreign_request: 0, excluded_source_run: 0, not_social: 0, no_url: 0, no_text: 0,\n    system_event: 0, off_topic: 0, low_relevance: 0, duplicate: 0, over_cap: 0 };\n  var perSource = {};\n  var seenPost = {};\n  var out = [];\n  var sources = {};\n  var considered = 0;\n\n  rows.forEach(function (r) {\n    if (!r || typeof r !== 'object') return;\n    considered++;\n\n    // 1. request scope \u2014 a row from another request may never enter this report.\n    var rowReq = seStr(r.agent_request_id), rowRun = seStr(r.source_run_id);\n    if (!(seFamMatch(rowReq, reqId) || seFamMatch(rowRun, reqId))) { dropped.foreign_request++; return; }\n    // A row that names NEITHER a request nor a run cannot be proven to belong here.\n    if (!rowReq && !rowRun) { dropped.foreign_request++; return; }\n\n    // 2. source-run scope \u2014 honour the WF16 quality gate when it produced a list.\n    if (includedRuns.length && includedRuns.indexOf(rowRun) < 0) { dropped.excluded_source_run++; return; }\n\n    // 3. social platforms only\n    var platform = seLow(r.platform);\n    if (platforms.indexOf(platform) < 0) { dropped.not_social++; return; }\n\n    // 4. citable: a public post URL and verbatim text\n    var url = seStr(r.post_url) || seStr(r.source_url) || seStr(r.profile_url);\n    if (!url) { dropped.no_url++; return; }\n    var rawText = seStr(r.text_context) || seStr(r.comment_text);\n    if (!seStr(rawText).trim()) { dropped.no_text++; return; }\n\n    // 5. post-level relevance decided by the POST TEXT alone (DEC-133/135)\n    if (seIsSystemEvent(rawText)) { dropped.system_event++; return; }\n    var service = seDeriveService(rawText);\n    if (service === 'unknown') { dropped.off_topic++; return; }\n    // WIP3-C: general finance vocabulary is insufficient \u2014 off-domain macro/bond/rating news is not primary evidence.\n    if (!seNichePrimary(rawText, service)) { dropped.low_relevance++; return; }\n\n    var excerpt = seClean(rawText, maxChars);\n    if (!excerpt) { dropped.no_text++; return; }\n\n    // dedup on the post URL \u2014 the same post must never be cited twice\n    var dkey = seStr(r.dedup_key) || url;\n    if (seenPost[dkey]) { dropped.duplicate++; return; }\n    seenPost[dkey] = true;\n\n    var key = seSourceKeyOf(r);\n    perSource[key] = seNum(perSource[key], 0);\n    if (perSource[key] >= maxPerSource) { dropped.over_cap++; return; }\n    if (out.length >= maxTotal) { dropped.over_cap++; return; }\n    perSource[key]++;\n\n    if (!sources[key]) {\n      sources[key] = {\n        source_key: key, platform: platform, source_kind: platform,\n        source_name: seClean(r.profile_name, 120) || key,\n        profile_url: seStr(r.profile_url) || seStr(r.source_url),\n        source_run_id: rowRun, agent_request_id: rowReq, records: 0\n      };\n    }\n    sources[key].records++;\n\n    out.push({\n      // bundle.evidence contract (consumed by analysis_bridge)\n      evidence_id: seStr(r.record_id) || (key + '#' + out.length),\n      competitor: sources[key].source_name,\n      url: url,\n      excerpt: excerpt,\n      finding: 'social_post',\n      collected_at: seStr(r.published_at) || seStr(r.created_at),\n      source_quality: 'accepted',\n      // social lineage the deterministic layer must keep for the report/XLSX\n      source_key: key,\n      source_kind: platform,\n      platform: platform,\n      source_run_id: rowRun,\n      agent_request_id: rowReq,\n      record_id: seStr(r.record_id),\n      derived_service: service,\n      profile_url: sources[key].profile_url\n    });\n  });\n\n  return {\n    evidence: out,\n    sources: Object.keys(sources).map(function (k) { return sources[k]; }),\n    considered: considered,\n    dropped: dropped\n  };\n}\n  return { buildSocialEvidence };\n})();\nconst { buildSocialEvidence } = __SE;\n// --- END embedded n8n/lib/social_evidence.js ---\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// Deterministic Report Builder core v0.3 (DEC-112/118/122/128): latest WF10 snapshot + website snapshots\n// + public lead signals -> 25-col report row + stakeholder Markdown. Facts only \u2014 every number/name comes\n// from sheet rows; nothing is invented; no contacts are surfaced; no outreach recommendations.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction num(v){var n=Number(v);return isFinite(n)?n:0;}\nfunction low(v){return str(v).toLowerCase();}\nfunction cut(s,n){s=str(s);return s.length>n?(s.slice(0,n-1)+'\u2026'):s;}\n// RU pluralizer (1 \u0441\u0430\u0439\u0442 / 2 \u0441\u0430\u0439\u0442\u0430 / 5 \u0441\u0430\u0439\u0442\u043e\u0432) \u2014 used for honest user-facing counts (B5).\nfunction plur(n,one,few,many){n=Math.abs(Number(n)||0);var n1=n%10,n2=n%100;if(n2>10&&n2<20)return many;if(n1>1&&n1<5)return few;if(n1===1)return one;return many;}\nfunction stripMd(s){s=str(s);if(!s)return s;s=s.replace(/\\[([^\\]]*)\\]\\((https?:[^)]*)\\)/g,'$1');s=s.replace(/\\]\\(\\s*https?:[^)\\s]*\\)?/g,'');s=s.replace(/\\[[^\\]]*$/,'');s=s.replace(/[\\[\\]]+/g,'').replace(/\\\\+/g,'').replace(/\\s{2,}/g,' ').trim();return s;}\n// Defect A (C6): deterministic report-layer contact redaction. Removes direct contact identifiers that may be\n// embedded in lead evidence text (phones, @handles, t.me/VK profile links, emails) BEFORE truncation/formatting,\n// while preserving business numbers (amounts, %, rates, dates) and ordinary source-POST URLs (vk.com/wall...).\nconst CONTACT_MARK='[PUBLIC CONTACT REDACTED]';\nfunction redact(s){ s=str(s); if(!s)return s;\n  s=s.replace(/[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}/g,CONTACT_MARK);            // emails (before @handle)\n  s=s.replace(/(?:\\+7|8)[\\s\\-()]*\\d{3}[\\s\\-()]*\\d{3}[\\s\\-]*\\d{2}[\\s\\-]*\\d{2}/g,CONTACT_MARK); // RU phones\n  s=s.replace(/\\+\\d[\\d\\s().\\-]{8,}\\d/g,CONTACT_MARK);                                       // intl phone-like\n  s=s.replace(/(?:https?:\\/\\/)?(?:t|telegram)\\.me\\/[A-Za-z0-9_+]+/gi,CONTACT_MARK);          // telegram links\n  s=s.replace(/@[A-Za-z0-9_]{3,32}/g,CONTACT_MARK);                                            // @handles\n  s=s.replace(/(?:https?:\\/\\/)?(?:www\\.)?vk\\.com\\/(?!wall)[A-Za-z0-9_.]+/gi,CONTACT_MARK);   // VK profiles/groups (keep wall posts)\n  return s;\n}\nfunction rowsOf(name){try{return $(name).all().map(i=>i.json).filter(r=>r&&Object.keys(r).some(k=>str(r[k])!==''));}catch(e){return [];}}\nconst cfg=$('Set Report Config').first().json;\nconst topN=Number(cfg.top_n)||3;\nconst maxOffer=Number(cfg.max_offer_chars)||90;\nconst profiles=rowsOf('Read competitor_profiles');\nconst angles=rowsOf('Read market_angles');\nconst signals=rowsOf('Read audience_activity_signals');\nconst plans=rowsOf('Read content_positioning_plan').filter(p=>str(p.plan_id));\nconst siteSnaps=rowsOf('Read competitor_site_snapshots');\nconst __reqId=str(cfg.agent_request_id);\nfunction __famMatch(a,b){a=str(a);b=str(b);if(!b)return true;if(!a)return false;return a===b||a.indexOf(b+'::')===0||b.indexOf(a+'::')===0;}\n// REPORT-LEADS-001: public_lead_signals accumulates across ALL runs. Count ONLY this request's leads (join on\n// agent_request_id/run_id) AND require a real lead_signal_id + non-empty evidence, so stale/blank rows can never\n// inflate the public-lead count (the phantom '999') or render empty \u00ab\u00bb rows.\nfunction __leadInReq(l){var a=str(l.agent_request_id),r=str(l.run_id);if(!a&&!r)return true;return __famMatch(a,__reqId)||__famMatch(r,__reqId);}\nfunction __leadValid(l){return str(l.lead_signal_id)!==''||str(l.evidence_excerpt||l.text_evidence||l.evidence_text)!==''||str(l.intent_type)!==''||num(l.lead_score)>0;}\nconst leadSignals=rowsOf('Read public_lead_signals').filter(function(l){return __leadInReq(l)&&__leadValid(l);});\n// --- Stage C Closure Patch 2: WF16 source_health enforcement (\u00a711.5) ---\nconst healthRows=rowsOf('Read source_health');\nconst __gate=buildEligibility(healthRows, cfg);\nconst included_source_runs=__gate.eligible_run_ids;\nconst excluded_source_runs=__gate.excluded_run_ids;\nconst report_contains_degraded_data=__gate.has_degraded_included;\n// SOCIAL-BRIDGE-001: verbatim Telegram/VK posts become citable evidence for SOURCE analysis. Strictly scoped to\n// this request AND to the source runs the WF16 quality gate admitted; post-level relevance (DEC-133/135) and\n// bounding/redaction live in the embedded canonical lib. Social sources deliberately never become competitor\n// profiles, so without this the evidence never reached WF28 (production req_76722076, execs 1096/1101: 22\n// relevant posts -> 0 analysis targets).\nconst __social=buildSocialEvidence(rowsOf('Read raw_market_records'),{agent_request_id:__reqId,included_source_runs:included_source_runs},{max_total:num(cfg.max_social_evidence)||40,max_per_source:num(cfg.max_social_evidence_per_source)||12,max_excerpt_chars:num(cfg.max_social_excerpt_chars)||600});\nconst socialEvidence=__social.evidence;\nconst socialSources=__social.sources;\nconst report_contains_fixture_data=(cfg.allow_fixture_report===true)&&__gate.has_fixture_warning;\nconst isFixtureReport=(cfg.allow_fixture_report===true)||['fixture','manual_test'].indexOf(low(cfg.report_data_mode))>=0||report_contains_fixture_data;\nconst quality_warning=(report_contains_degraded_data?'DEGRADED source data included by explicit opt-in \u2014 verify before acting. ':'')+(isFixtureReport?'TEST/FIXTURE data \u2014 not production intelligence. ':'');\n// --- run selection: plan_id carries the WF10 run stamp (plan_<stamp> or plan_<stamp>_no_data; one plan per run)\nfunction planStamp(p){return str(p.plan_id).replace(/^plan_/,'').replace(/_no_data$/,'');}\nplans.sort((a,b)=>planStamp(a)<planStamp(b)?-1:1);\nif(!plans.length){throw new Error('WF12: content_positioning_plan is empty \u2014 run WF10 at least once before building a report.');}\nconst latest=plans[plans.length-1];\nconst stamp=planStamp(latest);\n// --- Stage C Closure Patch 2: compatible trend baseline (\u00a711.6, S3-D7) \u2014 never mark all NEW on no baseline.\nconst __baselineCandidates=plans.filter(p=>planStamp(p)!==stamp).map(function(p){var rm=str(p.source_evidence).match(/rows=(\\d+)/);var rows=rm?Number(rm[1]):0;var nd=/_no_data$/.test(str(p.plan_id))||rows===0;return {run_stamp:planStamp(p),niche_id:p.niche,region:p.region,service_filter:'',data_mode:nd?'no_data':'live',report_eligible:!nd,rows_after_filters:rows,time_window_days:30};});\nconst __bl=selectBaseline(__baselineCandidates,{run_stamp:stamp,niche_id:str(cfg.niche_id),region:str(cfg.region),time_window_days:30},{});\nconst trend_status=__bl.trend_status;\nconst hasBaseline=!!__bl.baseline;\nconst prevStamp=__bl.baseline_run_stamp;\nconst isNoData=/_no_data$/.test(str(latest.plan_id))||/rows=0/.test(str(latest.source_evidence));\nconst rowsAfter=(function(){const m=str(latest.source_evidence).match(/rows=(\\d+)/);return m?Number(m[1]):0;})();\n// --- snapshot filters: WF10 ids/notes carry the run stamp\nconst __ARID=str(cfg.agent_request_id||cfg.agent_request_id_filter);const __aridOk=function(r){const v=str(r&&r.agent_request_id);return !__ARID||!v||v===__ARID;};\nlet profNow=profiles.filter(p=>(str(p.notes).indexOf('wf10_'+stamp)>=0||str(p.notes).indexOf(stamp)>=0)&&__aridOk(p));\nlet angNow=angles.filter(a=>str(a.angle_id).endsWith(stamp)&&__aridOk(a));\nconst angPrev=prevStamp?angles.filter(a=>str(a.angle_id).endsWith(prevStamp)):[];\nlet sigNow=signals.filter(s=>str(s.signal_id).endsWith(stamp)&&__aridOk(s));\n// --- Stage C Closure Patch 3: defensive body gate. The rendered body must contain ONLY records whose\n// stamped source lineage is eligible under the CURRENT source_health (Obj1.10/1.11). A WF10 snapshot\n// row built from a now-quarantined / fixture / report_ineligible source run is dropped from the body,\n// keeping the body and the source-quality section consistent.\nfunction __bodyEligible(row){\n  var ids=String(row.source_run_ids||row.source_run_id||'').split(/\\s*,\\s*/).filter(Boolean);\n  if(ids.length===0) return rowEligible(row,__gate,cfg).eligible;\n  for(var i=0;i<ids.length;i++){ if(!rowEligible({source_run_id:ids[i],data_mode:row.data_mode,report_eligible:row.report_eligible,quality_status:row.quality_status,review_status:row.review_status},__gate,cfg).eligible) return false; }\n  return true;\n}\nconst __profBefore=profNow.length, __angBefore=angNow.length, __sigBefore=sigNow.length;\nprofNow=profNow.filter(__bodyEligible); angNow=angNow.filter(__bodyEligible); sigNow=sigNow.filter(__bodyEligible);\nconst body_records_excluded=(__profBefore-profNow.length)+(__angBefore-angNow.length)+(__sigBefore-sigNow.length);\n// REPORT-QUALITY-v2 (RQ-ENTITY-001): decode HTML entities so a user never sees raw &#33; / &amp; / &#39; etc.\nfunction decodeEntities(s){s=str(s);if(!s)return s;return s\n  .replace(/&#(\\d+);/g,function(_,n){try{return String.fromCodePoint(Number(n));}catch(e){return _;}})\n  .replace(/&#x([0-9a-fA-F]+);/g,function(_,n){try{return String.fromCodePoint(parseInt(n,16));}catch(e){return _;}})\n  .replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'\"').replace(/&#39;|&apos;/g,\"'\").replace(/&nbsp;/g,' ');}\n// REPORT-QUALITY-v2 (RQ-PLACEHOLDER-001): internal/generic offer placeholders that must NEVER reach the user\n// (mirrors WF16 generic_offer). A telegram competitor post with only ad-copy has no extracted offer -> render '' (omitted).\nvar OFFER_PLACEHOLDERS=['competitor channel ad copy','channel ad copy','market context','competitor channel'];\nfunction realOffer(s){s=decodeEntities(str(s));if(!s)return '';var l=s.toLowerCase();for(var i=0;i<OFFER_PLACEHOLDERS.length;i++){if(l===OFFER_PLACEHOLDERS[i])return '';}return s;}\n// AVITO-BLOCK-001: never advertise Avito while it is operator-infra-blocked (MS_AVITO_ENABLED!=='true').\nvar __avitoOn=(typeof $env!=='undefined'&&$env&&String($env.MS_AVITO_ENABLED).toLowerCase()==='true');\n// --- v0.3 clean competitor names (DEC-128): never show ugly '(unnamed) <long offer>' to a stakeholder.\n// Fallback: '<Platform> offer: <short offer text>'.\nfunction capWord(s){s=str(s);return s?(s.charAt(0).toUpperCase()+s.slice(1)):s;}\nfunction cleanName(p){\n  const n=decodeEntities(str(p.competitor_name));\n  if(n&&n.indexOf('(unnamed)')!==0)return cut(n,60);\n  const plat=capWord(str(p.platforms).split(/\\s*,\\s*/)[0]||'market');\n  const offer=cut(realOffer(str(p.offers).split(' | ')[0])||n.replace(/^\\(unnamed\\)\\s*/,''),60);\n  return plat+' offer: '+(offer||'(no offer text)');\n}\n// --- deterministic top lists\nconst topCompRows=profNow.slice().sort((a,b)=>(num(b.evidence_count)-num(a.evidence_count))||(num(b.source_confidence_score)-num(a.source_confidence_score))).slice(0,topN);\n// B5: no raw \"evidence N / conf N\" tokens in user-facing text \u2014 Russian, human-readable, correctly pluralized.\nconst topComp=topCompRows.map(function(p){var ev=num(p.evidence_count);return cleanName(p)+' (\u0443\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u044c '+num(p.source_confidence_score)+'/100, '+ev+' '+plur(ev,'\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e\u0449\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a','\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e\u0449\u0438\u0445 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430','\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e\u0449\u0438\u0445 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432')+')';});\nfunction angleKey(a){return str(a.angle_id).replace(/^angle_/,'').replace(/_\\d{8}_\\d{6}$/,'');}\nconst prevFreq={};for(const a of angPrev){prevFreq[angleKey(a)]=num(a.frequency);}\nconst topAng=angNow.slice().sort((a,b)=>num(b.frequency)-num(a.frequency)).slice(0,topN)\n  .map(a=>{const k=angleKey(a);const f=num(a.frequency);\n    const marker=trendMarker(f, hasBaseline?prevFreq[k]:undefined, hasBaseline);\n    return str(a.angle_text)+' '+freqSuffix(f, marker);});\n// RQ-TREND-001: only CLAIM a comparison with the previous period when concrete deltas are actually rendered\n// (a baseline exists AND there are ranked angles carrying up/down/flat/new markers). Otherwise say so honestly.\nconst __angMarkers=angNow.slice().sort((a,b)=>num(b.frequency)-num(a.frequency)).slice(0,topN).map(a=>trendMarker(num(a.frequency), hasBaseline?prevFreq[angleKey(a)]:undefined, hasBaseline));const __realDeltas=__angMarkers.filter(m=>m==='up'||m==='down').length;const hasConcreteTrend=hasBaseline&&__realDeltas>0;\nconst audSummary=sigNow.map(s=>str(s.platform)+': \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432 '+num(s.question_count)+', \u0432\u043e\u0437\u0440\u0430\u0436\u0435\u043d\u0438\u0439 '+num(s.objection_count)+', \u0436\u0430\u043b\u043e\u0431 '+num(s.complaint_count)+', \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438\u0439 \u043a\u0443\u043f\u0438\u0442\u044c '+num(s.buying_intent_count)+(str(s.top_pains)?(', \u0431\u043e\u043b\u0438: '+str(s.top_pains)):'')).join(' | ');\nconst NO_DATA_TEXT='no_data; broaden filters or source scope'; // internal diagnostic (notes column only \u2014 NEVER user-facing)\n// RQ-NODATA-001: user-facing no-data explanation \u2014 concise Russian, no internal enums / English / \"no_data\".\nconst NO_DATA_RU='\u0417\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0435 \u043e\u043a\u043d\u043e \u043d\u043e\u0432\u044b\u0445 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.';\n// --- v0.3 shortened offers/prices (RQ-PLACEHOLDER-001: omit rows whose only \"offer\" is an internal placeholder)\nconst offersPrices=profNow.map(function(p){\n  const off=cut(realOffer(str(p.offers).split(' | ')[0]),maxOffer);\n  const pr=cut(decodeEntities(str(p.prices_terms).split(' | ')[0]||''),maxOffer);\n  if(!off&&!pr)return '';                                  // no real offer AND no price -> omit (no placeholder leak)\n  const body=off||pr; const tail=(off&&pr)?(' \u2014 '+pr):'';\n  return cleanName(p)+': '+body+tail;\n}).filter(Boolean).slice(0,5);\nconst confScores=profNow.map(p=>num(p.source_confidence_score)).filter(n=>n>0);\nconst confAvg=confScores.length?Math.round(confScores.reduce((a,b)=>a+b,0)/confScores.length):0;\n// RQ-COUNTS-001: user-facing Russian, no internal rule-name token.\nconst confLine=confScores.length?('\u0432 \u0441\u0440\u0435\u0434\u043d\u0435\u043c '+confAvg+'/100 (\u043c\u0438\u043d '+Math.min.apply(null,confScores)+', \u043c\u0430\u043a\u0441 '+Math.max.apply(null,confScores)+'; \u043f\u043e '+confScores.length+' \u043f\u0440\u043e\u0444\u0438\u043b\u044f\u043c)'):'(\u043d\u0435\u0442 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439 \u0432 \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0435)';\nconst platformsSeen={};profNow.forEach(p=>str(p.platforms).split(/\\s*,\\s*/).forEach(x=>{if(x)platformsSeen[x]=(platformsSeen[x]||0)+1;}));\nconst platLine=Object.keys(platformsSeen).map(k=>k+' ('+platformsSeen[k]+')').join(', ')||'(none)';\n// --- v0.3 competitor WEBSITE block (Stage 2 reintegration, DEC-129): latest snapshot per domain.\nlet website_snapshots_excluded=0;\nfunction __snapDegraded(s){var q=low(s.quality_status);if(q==='degraded'||q==='quarantined')return true;if(s.report_eligible===false||low(s.report_eligible)==='false')return true;if(/raw_markdown|raw_md|degraded/.test(low(str(s.fallback_reason)+' '+str(s.quality_flags))))return true;return false;}\n// report_data_mode=live drops snapshots that declare a non-live data_mode; lineage-carrying snapshots are held to the same canonical gate as body records (profile/snapshot eligibility parity, A4).\nfunction __snapModeExcluded(s){var dm=low(s.data_mode);if((low(cfg.report_data_mode)||'live')==='live'&&(dm==='fixture'||dm==='manual_test'))return true;var hasLin=String(s.source_run_ids||s.source_run_id||'').trim()!=='';if(hasLin&&!__bodyEligible(s))return true;return false;}\n// SCOPE-SITES-001 (B1): the site block + \"\u0441\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432\" count must reflect only THIS request's sites, not\n// every historical website snapshot in the base. Current-run domains = websites the user explicitly supplied this\n// request UNION domains that actually produced a profile in this run (profNow \u2014 already stamp+request scoped).\n// Every other saved snapshot is HISTORICAL CONTEXT: surfaced in a clearly-labeled block, never a current count.\n// When there is NO current-run signal at all we keep the prior behaviour (show saved snapshots) so a legitimate\n// historical / no-data report is never blanked.\nfunction __hostOf(u){return low(String(u).replace(/^https?:\\/\\//,'').replace(/^www\\./,'').replace(/[\\/?#].*$/,''));}\nvar __suppliedScope={};try{__suppliedScope=JSON.parse(str(cfg.supplied_sources)||'{}');}catch(e){__suppliedScope={};}\nvar __curDomains={};\n(Array.isArray(__suppliedScope.websites)?__suppliedScope.websites:[]).forEach(function(u){var h=__hostOf(u);if(h)__curDomains[h]=true;});\nprofNow.forEach(function(p){str(p.source_urls).split(/[\\s,;]+/).forEach(function(u){var h=__hostOf(u);if(h)__curDomains[h]=true;});});\nvar __curRunIds={};\nprofNow.forEach(function(p){String(p.source_run_ids||p.source_run_id||'').split(/\\s*,\\s*/).forEach(function(id){id=str(id);if(id)__curRunIds[id]=true;});});\n// A snapshot is HISTORICAL (not this request) only when we can positively tell so: its domain is not one the user\n// supplied / that produced a profile this run, AND either it carries a source_run_id from a different run, or we\n// have a reliable current-domain allowlist that it is absent from. A snapshot with no lineage id and no allowlist\n// to compare against stays CURRENT (old behaviour) so a legitimate report is never blanked.\nvar __haveDomainAllowlist=Object.keys(__curDomains).length>0;\nvar __hasScopeSignal=__haveDomainAllowlist||(Object.keys(__curRunIds).length>0)||(__suppliedScope.explicit===true);\nfunction __snapCurrent(s,d){d=low(d);if(__curDomains[d])return true;var rid=str(s.source_run_id||s.run_id||s.agent_request_id);if(rid)return !!__curRunIds[rid];return !__haveDomainAllowlist;}\nconst byDomain={};        // CURRENT-run website snapshots (latest per domain)\nconst byDomainHist={};    // HISTORICAL snapshots present in the base but not part of this request\nfor(const s of siteSnaps){const d=low(s.domain)||low(str(s.source_url).replace(/^https?:\\/\\/(www\\.)?/,'').split('/')[0]);if(!d)continue;\n  if(__snapDegraded(s)||__snapModeExcluded(s)){website_snapshots_excluded++;continue;}\n  var __bkt=(!__hasScopeSignal||__snapCurrent(s,d))?byDomain:byDomainHist;\n  if(!__bkt[d]||str(s.created_at)>str(__bkt[d].created_at))__bkt[d]=s;}\nconst histDomains=Object.keys(byDomainHist).filter(function(d){return !byDomain[d];});\nvar __hasHist=histDomains.length>0;\n// In a no-data report EVERY saved snapshot is \"out of window\" (nothing was collected this run) \u2014 count them all.\nconst savedSnapCount=Object.keys(byDomain).length+histDomains.length;\n// B6: honest current-run source composition (drives source-mix wording \u2014 no false \"\u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435\" for a fresh single source).\nvar __curKinds=[];\nif(Object.keys(byDomain).length)__curKinds.push('\u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u044b');\nprofNow.forEach(function(p){var pl=low(str(p.platforms)+' '+str(p.ad_channels));if(/telegram|tg|t\\.me/.test(pl)&&__curKinds.indexOf('Telegram')<0)__curKinds.push('Telegram');if(/vk|\u0432\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0435/.test(pl)&&__curKinds.indexOf('VK')<0)__curKinds.push('VK');if(/avito|\u0430\u0432\u0438\u0442\u043e/.test(pl)&&__curKinds.indexOf('Avito')<0)__curKinds.push('Avito');if(/web|site|\u0441\u0430\u0439\u0442/.test(pl)&&__curKinds.indexOf('\u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u044b')<0)__curKinds.push('\u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u044b');});\nif(!__curKinds.length&&Object.keys(byDomainHist).length)__curKinds.push('\u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u044b');\nvar __mixRu=__curKinds.length?__curKinds.join(', '):'\u043d\u0435\u0442 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432 \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u043e\u043a\u043d\u0435';\nvar __anySocial=profNow.some(function(p){return /telegram|tg|vk|avito|\u0441\u043e\u0446\u0441\u0435\u0442|\u043a\u0430\u043d\u0430\u043b|\u0441\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432/.test(low(str(p.platforms)+' '+str(p.ad_channels)));});\nvar __profLabel=__anySocial?'\u0421\u043e\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432':'\u041f\u0440\u043e\u0444\u0438\u043b\u0435\u0439 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432';\nconst siteLines=Object.keys(byDomain).slice(0,5).map(d=>{const s=byDomain[d];\n  return d+' \u2014 '+(str(s.company_name)||'(company n/a)')+': '+cut(stripMd(str(s.offer_summary))||'(offer n/a)',maxOffer)\n   +(str(s.prices_terms)?(' \u00b7 \u0446\u0435\u043d\u044b: '+cut(stripMd(s.prices_terms),60)):'')\n   +(str(s.guarantees)?(' \u00b7 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438: '+cut(stripMd(s.guarantees),60)):'')\n   +(str(s.cta_text)?(' \u00b7 CTA: '+cut(stripMd(s.cta_text),50)):'')\n   +(str(s.change_type)&&low(s.change_type)!=='baseline'?(' \u00b7 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435: '+({changed:'\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435',new_offer:'\u043d\u043e\u0432\u044b\u0439 \u043e\u0444\u0444\u0435\u0440',price_change:'\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0446\u0435\u043d\u044b',removed:'\u0441\u043d\u044f\u0442\u043e'}[low(s.change_type)]||str(s.change_type))):'');});\n// B6 (RQ-OFFER-SNAP-001): a current website snapshot carrying a real offer/price must never read as \"\u043e\u0444\u0444\u0435\u0440\u044b \u043d\u0435 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u044b\".\nvar __siteOffers=Object.keys(byDomain).map(function(d){var s=byDomain[d];var off=cut(stripMd(str(s.offer_summary)),maxOffer);var pr=cut(stripMd(str(s.prices_terms)),60);if(!off&&!pr)return '';return (str(s.company_name)||d)+': '+(off||('\u0446\u0435\u043d\u044b: '+pr))+((off&&pr)?(' \u2014 \u0446\u0435\u043d\u044b: '+pr):'');}).filter(Boolean);\nvar offersPricesAll=offersPrices.slice();__siteOffers.forEach(function(x){if(offersPricesAll.indexOf(x)<0)offersPricesAll.push(x);});offersPricesAll=offersPricesAll.slice(0,5);\nconst webProfiles=profNow.filter(p=>/web|site|\u0441\u0430\u0439\u0442/.test(low(str(p.platforms)+' '+str(p.ad_channels))));\nconst siteChanged=Object.keys(byDomain).filter(d=>['changed','new_offer','price_change'].indexOf(low(byDomain[d].change_type))>=0).length;\n// URL-INTAKE-002: when the user supplied explicit sources, scope the report to THEM \u2014 a per-source status list so\n// the reader never has to guess whether their finardi.ru / @channel / vk community was actually checked.\nvar __supplied={};try{__supplied=JSON.parse(str(cfg.supplied_sources)||'{}');}catch(e){__supplied={};}\nfunction __srcHitWeb(host){host=low(host);return Object.keys(byDomain).some(function(d){return low(d).indexOf(host)>=0||host.indexOf(low(d))>=0;})||profNow.some(function(p){return low(str(p.source_urls)).indexOf(host)>=0;});}\nfunction __srcHitHandle(h){h=low(h);return profNow.filter(function(p){return low(str(p.source_urls)+' '+str(p.competitor_name)+' '+str(p.notes)).indexOf(h)>=0;}).length;}\nvar __suppliedLines=[];\n(Array.isArray(__supplied.websites)?__supplied.websites:[]).forEach(function(u){var host=String(u).replace(/^https?:\\/\\//,'').replace(/\\/.*$/,'').toLowerCase();__suppliedLines.push(host+' \u2014 '+(__srcHitWeb(host)?'\u0441\u043e\u0431\u0440\u0430\u043d, \u0434\u043e\u043f\u0443\u0449\u0435\u043d \u043a \u0430\u043d\u0430\u043b\u0438\u0437\u0443':'\u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d, \u043d\u043e\u0432\u044b\u0445 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0444\u0430\u043a\u0442\u043e\u0432 \u0437\u0430 \u043e\u043a\u043d\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e'));});\n// NOTE: render channel/community handles WITHOUT @ / t.me/ / vk.com/ prefixes \u2014 the report-layer redact() would\n// otherwise treat a public @handle or t.me/vk.com link as a private contact and blank it out.\n(Array.isArray(__supplied.telegram)?__supplied.telegram:[]).forEach(function(h){var handle=String(h).replace(/^@/,'').toLowerCase();var c=__srcHitHandle(handle);__suppliedLines.push('Telegram-\u043a\u0430\u043d\u0430\u043b \u00ab'+handle+'\u00bb \u2014 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d'+(c?(', \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439: '+c):', \u043d\u043e\u0432\u044b\u0445 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0444\u0430\u043a\u0442\u043e\u0432 \u0437\u0430 \u043e\u043a\u043d\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e'));});\n(Array.isArray(__supplied.vk)?__supplied.vk:[]).forEach(function(v){var vk=String(v).replace(/^vk\\.com\\//,'').toLowerCase();var c=__srcHitHandle(vk);__suppliedLines.push('VK-\u0441\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432\u043e \u00ab'+vk+'\u00bb \u2014 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e'+(c?(', \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439: '+c):', \u043d\u043e\u0432\u044b\u0445 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0444\u0430\u043a\u0442\u043e\u0432 \u0437\u0430 \u043e\u043a\u043d\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e'));});\nvar __hasSupplied=(__supplied.explicit===true)&&__suppliedLines.length>0;\nvar __suppliedNames=[].concat((__supplied.websites||[]).map(function(u){return String(u).replace(/^https?:\\/\\//,'').replace(/\\/$/,'');}),(__supplied.telegram||[]).map(function(h){return 'Telegram-\u043a\u0430\u043d\u0430\u043b \u00ab'+String(h).replace(/^@/,'')+'\u00bb';}),(__supplied.vk||[]).map(function(v){return 'VK \u00ab'+String(v).replace(/^vk\\.com\\//,'')+'\u00bb';})).slice(0,3).join(', ');\nvar __suppliedSection=__hasSupplied?('\\n## \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438\\n'+__suppliedLines.map(function(l,i){return (i+1)+'. '+l;}).join('\\n')):'';\n// SCOPE-SITES-001 (B1): historical website snapshots that exist in the base but were NOT collected for this request\n// are shown ONLY here, clearly labeled \u2014 never mixed into the current-run counts or the \"\u0421\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432\" block.\nvar __histSection=__hasHist?('\\n## \u0418\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\\n- \u0412 \u0431\u0430\u0437\u0435 \u0435\u0441\u0442\u044c '+histDomains.length+' \u0434\u0440\u0443\u0433\u0438\u0445 \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u044b\u0445 '+plur(histDomains.length,'\u0441\u0430\u0439\u0442','\u0441\u0430\u0439\u0442\u0430','\u0441\u0430\u0439\u0442\u043e\u0432')+' \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 ('+histDomains.slice(0,5).join(', ')+(histDomains.length>5?' \u0438 \u0434\u0440.':'')+').\\n- \u041e\u043d\u0438 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u043a\u0430\u043a \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0441\u0431\u043e\u0440\u0430.'):'';\n// --- public lead / audience signal block (Lead Scout v0.3, Stage 3.5): AGGREGATES + anonymized summaries.\n// NO contacts/phones/usernames/profile URLs are ever surfaced \u2014 only counts + safe evidence excerpts.\n// Tolerates both v0.3 (review_status/source_platform/score_band/...) and the old 28-col schema (status/platform/...).\nfunction lStatus(l){return low(l.review_status||l.status);}\nfunction lPlatform(l){return low(l.source_platform||l.platform)||'unknown';}\nfunction lPriority(l){return low(l.review_priority);}\nfunction lBand(l){return low(l.score_band);}\nfunction lExcerpt(l){return cut(redact(str(l.evidence_excerpt||l.text_evidence||l.evidence_text)),140);}\nfunction lHasContact(l){return !!(str(l.public_phone)||str(l.public_username)||str(l.public_profile_url)||str(l.contact_public));}\nconst activeLeads=leadSignals.filter(l=>['dismissed','rejected','duplicate','stale'].indexOf(lStatus(l))<0);\nconst newLeads=activeLeads.filter(l=>lStatus(l)==='new'||lStatus(l)==='needs_review').length;\n// priority counts (fall back to band -> priority when review_priority absent)\nfunction prio(l){const p=lPriority(l);if(p)return p;const b=lBand(l);return b==='high'?'high':(b==='medium'?'medium':'low');}\nconst hiLeads=activeLeads.filter(l=>prio(l)==='high').length;\nconst medLeads=activeLeads.filter(l=>prio(l)==='medium').length;\nconst loLeads=activeLeads.filter(l=>prio(l)==='low').length;\n// --- Stage C Closure Patch 2: unambiguous contact counters (C1-D2). Contacts are NEVER rendered.\nconst contacts_detected=activeLeads.filter(l=>lHasContact(l)).length;\nconst contacts_excluded_from_report=contacts_detected;\nconst contacts_redacted=activeLeads.filter(l=>low(l.contact_use_policy)==='do_not_use'||/contact_blanked/.test(low(l.privacy_flags))).length;\nconst report_contains_contacts=false;\nconst contactsPresent=contacts_detected;\nconst contactsBlanked=contacts_redacted;\nconst intentCounts={};const painCounts={};\nfor(const l of activeLeads){const i=low(l.intent_type)||'unknown';intentCounts[i]=(intentCounts[i]||0)+1;\n  str(l.pain_type).split(/\\s*,\\s*/).forEach(pn=>{if(pn&&pn!=='unknown')painCounts[pn]=(painCounts[pn]||0)+1;});}\nconst intentLine=Object.keys(intentCounts).map(k=>k+'='+intentCounts[k]).join(', ')||'(none)';\nconst topLeadPains=Object.keys(painCounts).sort((a,b)=>painCounts[b]-painCounts[a]).slice(0,4).map(k=>k+' ('+painCounts[k]+')').join(', ')||'(none)';\nconst leadLine=activeLeads.length?('\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b: '+activeLeads.length+' (\u043d\u043e\u0432\u044b\u0445: '+newLeads+') \u00b7 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 \u0412/\u0421/\u041d: '+hiLeads+'/'+medLeads+'/'+loLeads+' \u00b7 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u043d\u0435 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u044e\u0442\u0441\u044f (\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e '+contacts_detected+', \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043e '+contacts_excluded_from_report+') \u00b7 \u0431\u043e\u043b\u0438: '+topLeadPains+' \u00b7 \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0443\u0447\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430, \u0431\u0435\u0437 \u0430\u0443\u0442\u0440\u0438\u0447\u0430'):(leadSignals.length?('\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b: '+leadSignals.length+' \u0441\u0442\u0440\u043e\u043a, \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0441\u0435\u0439\u0447\u0430\u0441 \u043d\u0435\u0442'):'(\u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 \u043f\u043e\u043a\u0430 \u043d\u0435\u0442)');\n// top 3-5 anonymized SAFE summaries (no contacts, no names, no profile URLs) \u2014 manager opens the row to verify.\nconst topLeads=activeLeads.slice().sort((a,b)=>(num(b.lead_score)-num(a.lead_score))).slice(0,5)\n  .map((l,i)=>(i+1)+'. ['+(lBand(l)||'-')+' '+num(l.lead_score)+'] '+lPlatform(l)+' \u00b7 '+(str(l.intent_type)||'-')+' \u00b7 \u0431\u043e\u043b\u044c: '+(str(l.pain_type)||'-')+' \u00b7 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435: '+(str(l.recommended_action)&&str(l.recommended_action)!=='manual_review'?str(l.recommended_action):'\u0440\u0443\u0447\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430')+(lHasContact(l)?' \u00b7 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u043e \u0435\u0441\u0442\u044c (\u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0443\u0447\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430)':'')+'\\n   \u00ab'+lExcerpt(l)+'\u00bb');\n// --- v0.3 stakeholder executive digest: 5-7 clean bullets, no jargon, no raw ids.\nconst digest=[];\nif(isNoData){digest.push('\u0414\u0430\u043d\u043d\u044b\u0445 \u0437\u0430 \u043e\u043a\u043d\u043e \u043e\u0442\u0447\u0451\u0442\u0430 \u043d\u0435\u0442 \u2014 \u043d\u0443\u0436\u043d\u043e \u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u0438\u043b\u0438 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438.');}\nelse{\n  digest.push(__profLabel+': '+profNow.length+(topCompRows.length?('; \u0441\u0430\u043c\u044b\u0439 \u0437\u0430\u043c\u0435\u0442\u043d\u044b\u0439 \u2014 '+cleanName(topCompRows[0])):(siteLines.length?('; \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u044b \u0432 \u044d\u0442\u043e\u043c \u0441\u0440\u0435\u0437\u0435 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u0441\u0430\u0439\u0442\u0430\u043c\u0438 ('+siteLines.length+') \u2014 \u0441\u043c. \u0440\u0430\u0437\u0434\u0435\u043b \u00ab\u0421\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432\u00bb'):''))+'.');\n  if(topAng.length)digest.push('\u0413\u043b\u0430\u0432\u043d\u044b\u0439 \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0439 \u0443\u0433\u043e\u043b \u0440\u044b\u043d\u043a\u0430: '+topAng[0]+'.');\n  const vk=sigNow.filter(s=>low(s.platform)==='vk')[0];\n  const anySig=vk||sigNow[0];\n  if(anySig)digest.push('\u0410\u0443\u0434\u0438\u0442\u043e\u0440\u0438\u044f ('+str(anySig.platform)+'): \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432 '+num(anySig.question_count)+', \u0432\u043e\u0437\u0440\u0430\u0436\u0435\u043d\u0438\u0439 '+num(anySig.objection_count)+', \u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 '+num(anySig.buying_intent_count)+(str(anySig.top_pains)?('; \u0431\u043e\u043b\u0438: '+cut(anySig.top_pains,90)):'')+'.');\n  if(activeLeads.length)digest.push('\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432: '+activeLeads.length+' ('+newLeads+' \u043d\u043e\u0432\u044b\u0445; \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 H/M/L '+hiLeads+'/'+medLeads+'/'+loLeads+') \u2014 \u043d\u0430 \u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043e\u043c, \u0431\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e-\u043b\u0438\u0431\u043e \u0430\u0443\u0442\u0440\u0438\u0447\u0430'+(contactsPresent?('; \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432-\u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432: '+contactsPresent):'')+'.');\n  if(siteLines.length)digest.push('\u0421\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432: '+siteLines.length+' \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0432 \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0435'+(siteChanged?(', \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439: '+siteChanged):'')+'.');\n  digest.push('\u0414\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u043d\u043e\u0441\u0442\u044c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432: '+(confScores.length?('\u0432 \u0441\u0440\u0435\u0434\u043d\u0435\u043c '+confAvg+'/100'):'\u043d\u0435\u0442 \u043e\u0446\u0435\u043d\u0451\u043d\u043d\u044b\u0445 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439')+'; \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0431\u043e\u0440: '+__mixRu+(__hasHist?'; \u0435\u0441\u0442\u044c \u0442\u0430\u043a\u0436\u0435 \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 (\u0441\u043c. \u0440\u0430\u0437\u0434\u0435\u043b \u043d\u0438\u0436\u0435)':'')+'.');\n  if(topAng.length)digest.push('\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c\u044b\u0439 \u0444\u043e\u043a\u0443\u0441 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430: '+cut(str((angNow.slice().sort((a,b)=>num(b.frequency)-num(a.frequency))[0]||{}).recommended_content_response),110)+'.');\n}\nconst digestMd=digest.slice(0,7).map(b=>'- '+b).join('\\n');\n// --- v0.3 action blocks (deterministic, no outreach)\nconst mgrActions=isNoData?['\u0420\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u0438\u043b\u0438 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437.']:[\n  (activeLeads.length?('\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c '+newLeads+' \u043d\u043e\u0432\u044b\u0445 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 (\u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u0432\u044b\u0441\u043e\u043a\u0438\u0439 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442). \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0443\u0447\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430, \u0431\u0435\u0437 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439.'):'\u0421\u043e\u0431\u0440\u0430\u0442\u044c \u0438 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b.'),\n  '\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0442\u043e\u043f-\u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0438 \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0435 \u0443\u0433\u043b\u044b: \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c, \u043a\u043e\u0433\u043e \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435.',\n  (siteLines.length?(siteChanged>0?('\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0430 \u0441\u0430\u0439\u0442\u0430\u0445 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 ('+siteChanged+' \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432).'):'\u0421\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432: \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 \u043d\u0435\u0442 \u2014 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043f\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044e \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f.'):'\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0430\u0439\u0442\u044b \u0442\u043e\u043f-\u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0432 \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 (\u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).')];\nconst contentActions=isNoData?[]:angNow.slice().sort((a,b)=>num(b.frequency)-num(a.frequency)).slice(0,3).map(a=>cut(str(a.recommended_content_response),130)).filter(Boolean);\n// AVITO-BLOCK-001: the Avito action line appears ONLY when Avito is actually enabled (MS_AVITO_ENABLED=true).\n// While blocked it is omitted entirely \u2014 never advertised as available planned collection.\nconst sourceActions=isNoData?['\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0441\u0431\u043e\u0440 \u043f\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u043c, \u0437\u0430\u0442\u0435\u043c \u0430\u043d\u0430\u043b\u0438\u0437.']:[\n  (__avitoOn?'Avito: \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u043f\u043b\u0430\u043d\u043e\u0432\u044b\u0439 \u0441\u0431\u043e\u0440 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u043b\u0438\u043c\u0438\u0442\u043e\u0432 \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438.':''),\n  'Telegram: \u0441\u0431\u043e\u0440 \u0438\u0437 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043a\u0430\u043d\u0430\u043b\u043e\u0432 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u0438 \u0444\u0438\u043b\u044c\u0442\u0440 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u043e\u0441\u0442\u0438; \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u043a\u0430\u043d\u0430\u043b\u043e\u0432.',\n  'VK: \u0441\u0431\u043e\u0440 \u0438\u0437 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u043f\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c\u0443 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e.',\n  (siteLines.length?'\u0421\u0430\u0439\u0442\u044b: \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u044b \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 (\u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).':'\u0421\u0430\u0439\u0442\u044b: \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c 3\u20135 \u0442\u043e\u043f-\u0441\u0430\u0439\u0442\u043e\u0432 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0432 \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 (\u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).')].filter(Boolean);\nfunction ol(arr){return arr.map((x,i)=>(i+1)+'. '+x).join('\\n')||'(\u043d\u0435\u0442)';}\n// --- mandatory source-mix label (DEC-108)\n// B6: honest, current-run-derived source mix (not a hardcoded \"historical\" claim). Historical is only appended\n// when the base actually holds unrequested snapshots \u2014 the bundle/XLSX then carries a truthful composition label.\nconst SOURCE_MIX=(__curKinds.length?('current: '+__curKinds.join('+')):'no_current_sources')+(__hasHist?' (+historical context)':'');\nconst reportType=isNoData?'no_data_notice':str(cfg.report_type);\n// --- deterministic Markdown render (v0.3 stakeholder report)\nconst md=[\n'# \u041e\u0442\u0447\u0451\u0442: \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u044b \u0438 \u0440\u044b\u043d\u043e\u043a \u2014 '+str(cfg.region),\n(isFixtureReport?'> **TEST / FIXTURE DATA \u2014 NOT PRODUCTION INTELLIGENCE**':''),\n(quality_warning?('> **QUALITY WARNING:** '+quality_warning):''),\n'',\n'- \u041d\u0438\u0448\u0430: \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440\u0438\u0434\u0436 \u00b7 \u0420\u0435\u0433\u0438\u043e\u043d: '+str(cfg.region)+(hasConcreteTrend?' \u00b7 \u0435\u0441\u0442\u044c \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0435 \u0441 \u043f\u0440\u043e\u0448\u043b\u044b\u043c \u043f\u0435\u0440\u0438\u043e\u0434\u043e\u043c':''),\n// RQ-COUNTS-001: distinct, user-clear counts instead of a single ambiguous \"\u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439\".\n// RQ-COUNTS-001 / RQ-NODATA-001: in a no-data report the headline must not imply \"everything is empty\" while\n// historical website snapshots exist \u2014 separate current-window counts from saved historical snapshots.\n(isNoData\n  ? (__hasSupplied\n      ? ('- \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432: '+__suppliedLines.length+' \u00b7 \u043d\u043e\u0432\u044b\u0445 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0444\u0430\u043a\u0442\u043e\u0432: 0')\n      : ('- \u041d\u043e\u0432\u044b\u0445 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0432 \u043e\u043a\u043d\u0435: 0'+(savedSnapCount?(' \u00b7 \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u044b\u0445 \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u043e\u0432 \u0441\u0430\u0439\u0442\u043e\u0432: '+savedSnapCount+' (\u0432\u043d\u0435 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u043e\u043a\u043d\u0430)'):'')))\n  : ('- \u041f\u0440\u043e\u0444\u0438\u043b\u0438 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432: '+profNow.length+' \u00b7 \u0441\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432: '+siteLines.length+' \u00b7 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b: '+activeLeads.length+' \u00b7 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u043f\u043e\u0441\u043b\u0435 \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432: '+rowsAfter)),\n'',\n'## \u0414\u0430\u0439\u0434\u0436\u0435\u0441\u0442 \u0434\u043b\u044f \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f',digestMd,\n__suppliedSection,\n__histSection,\n'',\nisNoData?([\n  '## \u0427\u0442\u043e \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e',\n  (__hasSupplied\n    ? ('\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u043b \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438: '+__suppliedNames+'. '+NO_DATA_RU+(__hasHist?' \u0412 \u0431\u0430\u0437\u0435 \u0435\u0441\u0442\u044c \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u044b\u0435 \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u044b \u0434\u0440\u0443\u0433\u0438\u0445 \u0441\u0430\u0439\u0442\u043e\u0432, \u043d\u043e \u043e\u043d\u0438 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0438\u0441\u044c \u043a\u0430\u043a \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u044d\u0442\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430.':''))\n    : (NO_DATA_RU+(savedSnapCount?(' \u0421\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u044b\u0435 \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u044b \u0441\u0430\u0439\u0442\u043e\u0432 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 ('+savedSnapCount+') \u0435\u0441\u0442\u044c \u0432 \u0431\u0430\u0437\u0435, \u043d\u043e \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u043e\u0442\u0447\u0451\u0442\u0430 \u043d\u0435 \u0432\u043e\u0448\u043b\u0438.'):''))),\n  '\u041f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432: '+healthRows.length+'; \u0441\u0442\u0440\u043e\u043a, \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u043f\u043e\u0434 \u043a\u0440\u0438\u0442\u0435\u0440\u0438\u0438, \u0432 \u044d\u0442\u043e\u043c \u043e\u043a\u043d\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.',\n  '',\n  '## \u0427\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c',\n  '1. \u0420\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u043f\u0435\u0440\u0438\u043e\u0434 \u0430\u043d\u0430\u043b\u0438\u0437\u0430.',\n  '2. \u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432.',\n  '3. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u0441\u0431\u043e\u0440 \u0434\u0430\u043d\u043d\u044b\u0445.'\n].join('\\n')):[\n'## \u0421\u043d\u0430\u043f\u0448\u043e\u0442 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432','\u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439 \u0432 \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0435: '+profNow.length+' \u00b7 \u043f\u043b\u043e\u0449\u0430\u0434\u043a\u0438: '+platLine,\n'','### \u0422\u043e\u043f \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432',topComp.length?topComp.map((c,i)=>(i+1)+'. '+c).join('\\n'):'(\u0432 \u044d\u0442\u043e\u043c \u0441\u0440\u0435\u0437\u0435 \u043d\u0435\u0442)',\n'','## \u0422\u043e\u043f \u043e\u0444\u0444\u0435\u0440\u043e\u0432 \u0438 \u0446\u0435\u043d',offersPricesAll.length?offersPricesAll.map((o,i)=>(i+1)+'. '+o).join('\\n'):'(\u043e\u0444\u0444\u0435\u0440\u044b/\u0446\u0435\u043d\u044b \u043d\u0435 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u044b \u0432 \u044d\u0442\u043e\u043c \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0435)',\n'','## \u0421\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432',siteLines.length?siteLines.map((s,i)=>(i+1)+'. '+s).join('\\n'):'(\u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u044b \u0441\u0430\u0439\u0442\u043e\u0432 \u043f\u043e\u043a\u0430 \u043d\u0435 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u044b \u2014 \u0441\u043c. \u0440\u0430\u0437\u0434\u0435\u043b \u00ab\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u043c\u00bb)',\nwebProfiles.length?('\u041a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u044b \u0441 \u0432\u0435\u0431-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c: '+webProfiles.map(p=>cleanName(p)).join('; ')):'',\n// RQ-EMPTY-001: omit the market-angles section entirely when there is nothing to show (no blank \"(\u0432 \u044d\u0442\u043e\u043c \u0441\u0440\u0435\u0437\u0435 \u043d\u0435\u0442)\" header).\n(topAng.length?('\\n## \u0422\u043e\u043f \u0443\u0433\u043b\u043e\u0432 \u0440\u044b\u043d\u043a\u0430\\n'+topAng.map((a,i)=>(i+1)+'. '+a).join('\\n')):''),\n'','## \u0410\u0443\u0434\u0438\u0442\u043e\u0440\u0438\u044f \u0438 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b',(audSummary||'(\u043d\u0435\u0442 \u0430\u0443\u0434\u0438\u0442\u043e\u0440\u043d\u044b\u0445 \u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 \u0432 \u044d\u0442\u043e\u043c \u0441\u0440\u0435\u0437\u0435)'),'',leadLine,\n(topLeads.length?('\\n### \u0422\u043e\u043f \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 (\u0430\u043d\u043e\u043d\u0438\u043c\u043d\u043e; \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u041d\u0415 \u0440\u0430\u0441\u043a\u0440\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u2014 \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438)\\n'+topLeads.join('\\n')):''),\n'_\u041b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b: \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430; \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b/\u0442\u0435\u043b\u0435\u0444\u043e\u043d\u044b/\u043f\u0440\u043e\u0444\u0438\u043b\u0438 \u0432 \u043e\u0442\u0447\u0451\u0442 \u043d\u0435 \u0432\u044b\u0432\u043e\u0434\u044f\u0442\u0441\u044f; \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0438 \u043b\u044e\u0431\u043e\u0439 \u043a\u043e\u043d\u0442\u0430\u043a\u0442 \u2014 \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043e\u043c._',\n// RQ-EMPTY-001: \"\u041f\u043b\u0430\u043d \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430\" removed \u2014 it duplicated \u00ab\u0422\u043e\u043f \u0443\u0433\u043b\u043e\u0432 \u0440\u044b\u043d\u043a\u0430\u00bb and was usually blank.\n'','## \u0414\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u043d\u043e\u0441\u0442\u044c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432',confLine\n].join('\\n'),\n'',\n'## \u041a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432',\n'- \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432 \u043e\u0446\u0435\u043d\u0435\u043d\u043e: '+healthRows.length+' \u00b7 \u0434\u043e\u043f\u0443\u0449\u0435\u043d\u043e: '+included_source_runs.length+' \u00b7 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043e: '+excluded_source_runs.length,\n'- \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043d\u0438\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432 \u043e\u0442\u0447\u0451\u0442\u0435: '+(report_contains_degraded_data?'\u0435\u0441\u0442\u044c':'\u043d\u0435\u0442')+' \u00b7 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u043d\u0435 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u044e\u0442\u0441\u044f'+(website_snapshots_excluded?(' \u00b7 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u043e\u0432 \u043f\u043e\u043d\u0438\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430: '+website_snapshots_excluded):'')+(body_records_excluded?(' \u00b7 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0432\u043d\u0435 \u043a\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u0432: '+body_records_excluded):''),\n'',\n'## \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u0438 \u0441\u043e\u0441\u0442\u0430\u0432 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432',\n'- \u0421\u043e\u0441\u0442\u0430\u0432 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432 (\u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0431\u043e\u0440): '+__mixRu+'.'+(__hasHist?' \u0412 \u0431\u0430\u0437\u0435 \u0442\u0430\u043a\u0436\u0435 \u0435\u0441\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u043f\u0440\u043e\u0448\u043b\u044b\u0445 \u043f\u0440\u043e\u0433\u043e\u043d\u043e\u0432; \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u044d\u0442\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043e\u043d\u0438 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b.':''),\n'- \u0422\u0440\u0435\u043d\u0434\u044b: '+(hasConcreteTrend?'\u0435\u0441\u0442\u044c \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0435 \u0441 \u043f\u0440\u043e\u0448\u043b\u044b\u043c \u043f\u0435\u0440\u0438\u043e\u0434\u043e\u043c (\u0441\u043c. \u043f\u043e\u043c\u0435\u0442\u043a\u0438 \u0434\u0438\u043d\u0430\u043c\u0438\u043a\u0438 \u0443 \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0445 \u0443\u0433\u043b\u043e\u0432)':'\u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0441 \u043f\u0440\u043e\u0448\u043b\u044b\u043c \u043f\u0435\u0440\u0438\u043e\u0434\u043e\u043c \u2014 \u043f\u043e\u0437\u0438\u0446\u0438\u0438 \u043d\u0435 \u043f\u043e\u043c\u0435\u0447\u0430\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u043d\u043e\u0432\u044b\u0435')+'.',\n'- \u041b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430; \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u2014 \u044d\u0442\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435, \u0430 \u041d\u0415 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f (\u043f\u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0435\u0439 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0435 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438).',\n'- \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u0432 \u043e\u0442\u0447\u0451\u0442 \u043d\u0435 \u0432\u044b\u0432\u043e\u0434\u044f\u0442\u0441\u044f; \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0438 \u043f\u043e \u0430\u0443\u0442\u0440\u0438\u0447\u0443 \u043d\u0435 \u0444\u043e\u0440\u043c\u0438\u0440\u0443\u044e\u0442\u0441\u044f.',\n'',\n'## \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430',ol(mgrActions),\n// RQ-EMPTY-001: render the ideas section only when there are concrete content ideas (no blank \"## \u0418\u0434\u0435\u0438 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430\\n(\u043d\u0435\u0442)\").\n(contentActions.length?('\\n## \u0418\u0434\u0435\u0438 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430\\n'+ol(contentActions)):''),\n'',\n'## \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u043c',ol(sourceActions),\n'',\n'_\u041e\u0442\u0447\u0451\u0442 \u0441\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d \u0438\u0437 \u0441\u043e\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445. \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u043d\u0435 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u044e\u0442\u0441\u044f, \u0430\u0443\u0442\u0440\u0438\u0447 \u043d\u0435 \u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u0442\u0441\u044f._'\n].join('\\n').replace(/\\n{3,}/g,'\\n\\n').trim();\n// --- 25-column market_intelligence_reports row (MARKET_INTELLIGENCE_REPORT_SCHEMA.md v0.3)\nreturn [{ json: {\n  report_id:str(cfg.report_id),\n  created_at:str(cfg.created_at),\n  report_type:reportType,\n  niche_id:str(cfg.niche_id),\n  region:str(cfg.region),\n  time_window_days:30,\n  wf10_run_id:'wf10_'+stamp,\n  prev_wf10_run_id:prevStamp?('wf10_'+prevStamp):'',\n  source_mix:SOURCE_MIX,\n  rows_after_filters:rowsAfter,\n  trend_status:trend_status,\n  source_runs_evaluated:healthRows.length,\n  source_runs_included:included_source_runs.length,\n  source_runs_excluded:excluded_source_runs.join(', '),\n  report_contains_degraded_data:report_contains_degraded_data,\n  report_contains_fixture_data:!!report_contains_fixture_data,\n  report_contains_contacts:false,\n  contacts_detected:contacts_detected,\n  contacts_redacted:contacts_redacted,\n  contacts_excluded_from_report:contacts_excluded_from_report,\n  quality_warning:str(quality_warning),\n  website_snapshots_excluded:website_snapshots_excluded,\n  body_records_excluded:body_records_excluded,\n  is_fixture_report:isFixtureReport,\n  top_competitors:isNoData?'':topComp.join(' | '),\n  top_angles:isNoData?'':topAng.join(' | '),\n  audience_summary:isNoData?'':redact(audSummary+(activeLeads.length?(' || '+leadLine):'')),\n  content_plan_ref:str(latest.plan_id),\n  report_markdown_path:'',\n  llm_status:'disabled',\n  llm_model:'',\n  llm_input_tokens:0,\n  llm_output_tokens:0,\n  llm_cost_usd:0,\n  llm_summary_ru:'',\n  llm_recommendations_ru:'',\n  llm_quality_flags:'',\n  delivered_to:'none',\n  // DELIVERY-CONTENT-001: the markdown lived only inside notes \u2014 expose it (redacted) so WF20's delivery\n  // shows the real report. EXPORT-BUNDLE-001: structured bundle for WF24 XLSX/CSV export (owner stamped by WF20).\n  report_markdown:redact(md),\n  report_bundle:JSON.stringify({\n    report_id:str(cfg.report_id),agent_request_id:str(cfg.agent_request_id),owner_user_id:'',\n    created_at:str(cfg.created_at),report_type:reportType,niche:str(cfg.niche_id),region:str(cfg.region),\n    time_window_days:30,source_mix:SOURCE_MIX,data_mode:str(cfg.report_data_mode)||'live',\n    run_ids:{wf10_run_id:'wf10_'+stamp},\n    summary:{competitors_found:profNow.length,sources_checked:healthRows.length,\n      quality_status:(report_contains_degraded_data?'degraded':'healthy'),\n      key_findings:topComp.slice(0,5),\n      key_recommendations:topAng.slice(0,5).map(function(a){return redact(str(a.angle_name||a.angle||a.notes)).slice(0,200);}),\n      external_calls:0,llm_primary_calls:0,source_cost_status:'unknown',llm_cost_status:'unknown'},\n    competitors:topCompRows.map(function(p){return {competitor:cleanName(p),domain:'',region:str(cfg.region),\n      positioning:redact(decodeEntities(str(p.observed_strengths||p.service_types))).slice(0,300),score:num(p.source_confidence_score),\n      quality:'',last_checked:str(p.last_seen_at),source_url:str(p.source_urls).split(/[;,\\s]+/)[0]||'',source_run_id:'wf10_'+stamp};}),\n    offers:topCompRows.map(function(p){return {p:p,off:realOffer(str(p.offers).split(' | ')[0]),pr:decodeEntities(str(p.prices_terms).split(' | ')[0]||'')};})\n      .filter(function(x){return x.off!==''||x.pr!=='';})   // RQ-PLACEHOLDER-001: no placeholder-only offer rows in the XLSX either\n      .map(function(x){var p=x.p;return {competitor:cleanName(p),\n      offer:redact(x.off).slice(0,300),price_rate:redact(x.pr).slice(0,200),amount_range:'',term:'',cta:'',\n      promotion:'',collected_at:str(p.last_seen_at),evidence_url:str(p.source_urls).split(/[;,\\s]+/)[0]||'',evidence_id:'',\n      source_record_id:str(p.competitor_id),source_run_id:'wf10_'+stamp};}),\n    evidence:socialEvidence,\n    recommendations:topAng.slice(0,5).map(function(a){return {recommendation:redact(str(a.angle_name||a.angle||a.notes)).slice(0,200),\n      priority:'medium',rationale:redact(str(a.example_titles||a.examples||'')).slice(0,200),linked_finding_ids:[],next_action:''};}),\n    source_quality:healthRows.filter(function(h){return __aridOk(h);}).map(function(h){return {source:str(h.source_key||h.source_allowlist||h.platform),\n      platform:str(h.platform),health_score:num(h.quality_score)/100,status:str(h.quality_status),freshness:str(h.evaluated_at),\n      exclusions:[],error:'',source_run_id:str(h.source_run_id)};}),\n    changes:[],\n    run_metadata:{calls:0,llm_primary:0,run_ids:['wf10_'+stamp],data_mode:str(cfg.report_data_mode)||'live',generated_at:str(cfg.created_at)}\n  }),\n  notes:(isNoData?('no_data_notice; recommendation: '+NO_DATA_TEXT+'; '):'')+'wf12 v0.3 stakeholder report (DEC-128); llm disabled by default (DEC-112); markdown inline below || '+redact(md)\n}}];"
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000i1",
      "name": "IF enable_llm_summary?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1100,
        320
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "llm-flag",
              "leftValue": "={{ $('Set Report Config').first().json.enable_llm_summary }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000g1",
      "name": "Claude Summary Approval Gate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        180
      ],
      "parameters": {
        "jsCode": "// CLAUDE SUMMARY APPROVAL GATE (DEC-112/122, WF12-LLMGATE-001) \u2014 a DECISION, not a throw. In agent chains a\n// Code-node throw kills WF12->WF20->WF18 and the user gets nothing; the correct degradation is the deterministic\n// report ($0). The gate stays fail-closed for the LLM: any unmet condition -> llm_gate_allowed=false and the\n// following IF routes straight to the report append. executeWorkflow passes booleans as STRINGS ('true') \u2014 accept\n// both. Claude remains the interpretation layer ON TOP of the deterministic fact core: no invented facts/contacts.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction boolish(v){return v===true||str(v).toLowerCase()==='true';}\nconst cfg=$('Set Report Config').first().json;\nconst _rep=(($input.all()[0]||{json:{}}).json)||{};\nlet allowed=true, reason='ok';\nif(!boolish(cfg.enable_llm_summary)){ allowed=false; reason='llm_summary_disabled'; }\nelse if(!(Number(_rep.rows_after_filters)>0)||_rep.report_eligible===false){ allowed=false; reason='no_eligible_facts'; }\nelse if(str(_rep.llm_summary_status)==='summarized'||_rep.already_summarized===true){ allowed=false; reason='already_summarized_idempotency'; }\nelse if(str(cfg.llm_approval_token)!=='I_APPROVE_CLAUDE_REPORT_SUMMARY'){ allowed=false; reason='approval_token_missing'; }\nreturn $input.all().map(it=>({json:Object.assign({},it.json,{llm_gate_allowed:allowed,llm_gate_reason:reason})}));"
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000a1",
      "name": "Append market_intelligence_reports",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1320,
        440
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "market_intelligence_reports",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000q1",
      "name": "Build agent_requests Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        440
      ],
      "parameters": {
        "jsCode": "// One agent_requests row (exactly 21 columns). status=completed. No external calls from this node.\n// v0.3: reads the APPENDED report row from $input so both branches (deterministic / Claude-merged) report\n// the real llm_status / token counts / cost in result_summary.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Report Config').first().json;\nconst det=$('Build Deterministic Report').first().json;\nconst rep=Object.assign({},det,($input.first()&&$input.first().json)||{});\nconst llmUsed=str(rep.llm_status)!==''&&str(rep.llm_status)!=='disabled';\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:'Build market intelligence report v0.3 from WF10 tabs + competitor_site_snapshots + public_lead_signals',\n  request_type:'report_summary',\n  source_scope:'business_queues',\n  platforms:'',\n  query:'wf10_run='+str(rep.wf10_run_id),\n  region:str(cfg.region),\n  service_focus:str(cfg.niche_id),\n  requested_limit:0,\n  status:'completed',\n  plan_summary:'WF12 Report Builder v0.3 (DEC-128): deterministic stakeholder report (executive digest, website block, public lead/audience block, action blocks) + optional approval/budget-gated Claude summary (DEC-112/122).',\n  estimated_source_cost_usd:0,\n  estimated_analysis_cost_usd:Number(rep.llm_cost_usd)||0,\n  approval_required:false,\n  approved_by:'',\n  approved_at:'',\n  result_summary:'report_id='+str(rep.report_id)+'; report_type='+str(rep.report_type)+'; wf10_run_id='+str(rep.wf10_run_id)+'; prev='+(str(rep.prev_wf10_run_id)||'none')+'; rows_after_filters='+rep.rows_after_filters+'; llm_status='+(str(rep.llm_status)||'disabled')+'; llm_input_tokens='+(Number(rep.llm_input_tokens)||0)+'; llm_output_tokens='+(Number(rep.llm_output_tokens)||0)+'; llm_cost_usd='+(Number(rep.llm_cost_usd)||0)+(str(rep.llm_quality_flags)?('; llm_quality_flags='+str(rep.llm_quality_flags)):'')+'; delivered_to=none.',\n  next_action:(str(rep.report_type)==='no_data_notice')?'no_data; broaden filters or source scope, then re-run WF10 and WF12.':(llmUsed?'Review the report row incl. llm_summary_ru/llm_quality_flags; verify Claude added no facts absent from the deterministic report; record cost in COSTS_AND_LIMITS.md.':'Review the market_intelligence_reports row; Telegram delivery and optional Claude summary are separate gated steps (DEC-112).'),\n  notes:'WF12 v0.3 stakeholder Report Builder; source_mix='+str(rep.source_mix)+'; llm '+(llmUsed?('used under approval + budget guard, cost_usd='+(Number(rep.llm_cost_usd)||0)):'disabled')+'; no Telegram send; no outreach.'\n}}];"
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000a2",
      "name": "Append agent_requests",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1760,
        440
      ],
      "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",
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000s1",
      "name": "Final Summary Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        440
      ],
      "parameters": {
        "jsCode": "// Final run summary. No external calls.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Report Config').first().json;\nlet rep=$('Build Deterministic Report').first().json;\ntry{ rep=Object.assign({},rep,$('Append market_intelligence_reports').first().json); }catch(e){}\nreturn [{ json: {\n  workflow:'12 - Market Intelligence Report Builder (v0.3 stakeholder report)',\n  report_id:str(rep.report_id),\n  report_type:str(rep.report_type),\n  wf10_run_id:str(rep.wf10_run_id),\n  prev_wf10_run_id:str(rep.prev_wf10_run_id)||'(none)',\n  rows_after_filters:rep.rows_after_filters,\n  top_competitors:str(rep.top_competitors),\n  top_angles:str(rep.top_angles),\n  report_markdown:(function(){var m=str(rep.report_markdown);var ls=str(rep.llm_summary_ru);\n    if(ls){m+='\\n\\n\\u0412\\u044b\\u0432\\u043e\\u0434\\u044b \\u0438 \\u0440\\u0435\\u043a\\u043e\\u043c\\u0435\\u043d\\u0434\\u0430\\u0446\\u0438\\u0438:\\n'+ls;\n      var lr=str(rep.llm_recommendations_ru);if(lr)m+='\\n'+lr;}\n    return m;})(),\n  summary_text:str(rep.llm_summary_ru),\n  report_bundle:str(rep.report_bundle),\n  source_mix:str(rep.source_mix),\n  llm_status:str(rep.llm_status)||'disabled',\n  llm_cost_usd:Number(rep.llm_cost_usd)||0,\n  llm_quality_flags:str(rep.llm_quality_flags),\n  telegram_send:false,\n  delivered_to:'none',\n  next_action:(str(rep.report_type)==='no_data_notice')?'no_data report written; broaden filters/source scope before the next WF10 run.':'Inspect market_intelligence_reports + agent_requests + live_source_runs rows. Trends need >=2 WF10 runs. Claude summary / Telegram delivery = separate gated steps.'\n}}];"
      }
    },
    {
      "parameters": {
        "jsCode": "// Build the evidence-bound Claude summary prompt v0.3 (DEC-122/128) + BUDGET GUARD (throws BEFORE the\n// HTTP node). FACTS ONLY: deterministic report fields + aggregate audience/lead-signal summaries \u2014 never\n// raw personal text dumps, never contact fields, never raw_market_records rows.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction cut(s,n){s=str(s);return s.length>n?s.slice(0,n):s;}\nconst cfg=$('Set Report Config').first().json;\nconst rep=$('Build Deterministic Report').first().json;\nconst sys='\u0422\u044b \u2014 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u043a \u0440\u044b\u043d\u043a\u0430 \u0434\u043b\u044f \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u043e-\u0431\u0440\u043e\u043a\u0435\u0440\u0441\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438. \u0422\u0435\u0431\u0435 \u0434\u0430\u043d \u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u042b\u0419 \u043e\u0442\u0447\u0451\u0442 (\u0444\u0430\u043a\u0442\u044b, \u0441\u043e\u0431\u0440\u0430\u043d\u043d\u044b\u0435 \u043f\u0430\u0439\u043f\u043b\u0430\u0439\u043d\u043e\u043c). \u0416\u0401\u0421\u0422\u041a\u0418\u0415 \u041f\u0420\u0410\u0412\u0418\u041b\u0410: (1) \u041d\u0415 \u0432\u044b\u0434\u0443\u043c\u044b\u0432\u0430\u0439 \u0444\u0430\u043a\u0442\u044b, \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432, \u0446\u0438\u0444\u0440\u044b, \u0446\u0435\u043d\u044b, \u0442\u0440\u0435\u043d\u0434\u044b; (2) \u041d\u0415 \u0443\u043f\u043e\u043c\u0438\u043d\u0430\u0439 \u0438 \u043d\u0435 \u0432\u044b\u0432\u043e\u0434\u0438 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b; (3) \u041d\u0415 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0439 \u0430\u0443\u0442\u0440\u0438\u0447/\u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438/\u043e\u0431\u0437\u0432\u043e\u043d\u044b \u0438 \u043d\u0435 \u0441\u0447\u0438\u0442\u0430\u0439 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0438 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c \u043d\u0430 \u043a\u043e\u043d\u0442\u0430\u043a\u0442; (4) \u0435\u0441\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u043c\u0430\u043b\u043e \u2014 \u043f\u0440\u044f\u043c\u043e \u0441\u043a\u0430\u0436\u0438 \u043e\u0431 \u044d\u0442\u043e\u043c \u0432 source_limitations; (5) \u043e\u0442\u0432\u0435\u0447\u0430\u0439 \u0422\u041e\u041b\u042c\u041a\u041e \u0432\u0430\u043b\u0438\u0434\u043d\u044b\u043c JSON-\u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c \u0431\u0435\u0437 markdown \u0438 \u043f\u043e\u044f\u0441\u043d\u0435\u043d\u0438\u0439, \u043a\u043b\u044e\u0447\u0438: executive_summary_ru (\u0441\u0442\u0440\u043e\u043a\u0430, \u0434\u043e 120 \u0441\u043b\u043e\u0432), key_findings (\u043c\u0430\u0441\u0441\u0438\u0432 \u0441\u0442\u0440\u043e\u043a, 3-6), market_risks (\u043c\u0430\u0441\u0441\u0438\u0432 \u0441\u0442\u0440\u043e\u043a, 2-4), recommended_next_actions (\u043c\u0430\u0441\u0441\u0438\u0432 \u0441\u0442\u0440\u043e\u043a, 3-5, \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f: \u043a\u043e\u043d\u0442\u0435\u043d\u0442, \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433, \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0434\u0430\u043d\u043d\u044b\u0445), content_recommendations (\u043c\u0430\u0441\u0441\u0438\u0432 \u0441\u0442\u0440\u043e\u043a, 2-4), source_limitations (\u043c\u0430\u0441\u0441\u0438\u0432 \u0441\u0442\u0440\u043e\u043a, 1-3). \u0412\u0441\u0451 \u043d\u0430 \u0440\u0443\u0441\u0441\u043a\u043e\u043c.';\nconst facts={ report_id:str(rep.report_id), niche:str(rep.niche_id), region:str(rep.region), wf10_run_id:str(rep.wf10_run_id), prev_wf10_run_id:str(rep.prev_wf10_run_id), rows_after_filters:rep.rows_after_filters, source_mix:str(rep.source_mix), top_competitors:str(rep.top_competitors), top_angles:str(rep.top_angles), audience_and_public_lead_signal_aggregates:str(rep.audience_summary), content_plan_ref:str(rep.content_plan_ref) };\n// --- budget guard (DEC-128): hard input cap + cost estimate BEFORE any HTTP call.\nconst maxInChars=Number(cfg.llm_max_input_chars)||8000;\nconst maxCost=Number(cfg.llm_max_estimated_cost_usd)||0.10;\nconst maxOut=Number(cfg.llm_max_tokens)||1200;\nconst userText='\u0414\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043e\u0442\u0447\u0451\u0442 (JSON, \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0444\u0430\u043a\u0442\u043e\u0432):\\n'+cut(JSON.stringify(facts),maxInChars);\nconst estInputTokens=Math.ceil((sys.length+userText.length)/4)+50;\nconst estCost=Math.round(((estInputTokens*(Number(cfg.llm_input_price_per_mtok)||3.0)+maxOut*(Number(cfg.llm_output_price_per_mtok)||15.0))/1000000)*10000)/10000;\nif(estCost>maxCost){\n  // WF12-LLMGATE-001: over-budget is a SKIP (deterministic report survives), never a chain-killing throw.\n  return [{ json: Object.assign({}, rep, { llm_budget_blocked: true, llm_gate_reason: 'over_budget_est_$'+estCost+'_max_$'+maxCost }) }];\n}\nconst body={ model:str(cfg.llm_model)||'claude-sonnet-4-6', max_tokens:maxOut, system:sys, messages:[{ role:'user', content:userText }] };\nreturn [{ json: Object.assign({}, rep, { llm_budget_blocked: false, llm_request_body: body, llm_facts_string: JSON.stringify(facts), llm_estimated_cost_usd: estCost, llm_estimated_input_tokens: estInputTokens }) }];"
      },
      "id": "wf12-llm-prompt",
      "name": "Build Claude Summary Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        180
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://aiprimetech.io/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.llm_request_body) }}",
        "options": {
          "timeout": 60000
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth"
      },
      "id": "wf12-llm-http-placeholder",
      "name": "Claude Summary API Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        180
      ],
      "disabled": false,
      "notes": "INERT (DEC-122): disabled, no real key. x-api-key value is a placeholder string \u2014 replace with an n8n credential (header auth) ONLY after operator approval. No live call was made when building this workflow.",
      "onError": "continueRegularOutput",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Merge the Claude summary into the report row + track cost + quality flags (DEC-122/128). If the HTTP\n// placeholder is still DISABLED, items pass through WITHOUT a Claude response body \u2014 this node then throws,\n// so no fabricated summary can ever be written. With a real response: parse the JSON sections, compute cost\n// (usage tokens x configured prices) and fill the llm_* columns of the 25-col report row.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction arr(v){return Array.isArray(v)?v.map(x=>str(x)).filter(Boolean):[];}\nconst cfg=$('Set Report Config').first().json;\nconst prompt=$('Build Claude Summary Prompt').first().json;\nconst rep=$('Build Deterministic Report').first().json;\nconst resp=$input.first().json;\nconst content=resp&&Array.isArray(resp.content)?resp.content:null;\nif(!content){\n  // WF12-LLMGATE-001: HTTP error / missing response -> deterministic fallback, never a chain-killing throw.\n  return [{ json: Object.assign({}, rep, { llm_status:'fallback_deterministic', llm_model:str(cfg.llm_model)||'claude-sonnet-4-6', llm_input_tokens:0, llm_output_tokens:0, llm_cost_usd:0, llm_summary_ru:'', llm_recommendations_ru:'', llm_quality_flags:'no_api_response; fell_back_to_deterministic', notes:str(rep.notes)+' || Claude summary unavailable (no API response) \u2014 deterministic report kept.' }) }];\n}\nlet text='';for(const b of content){ if(b&&b.type==='text'&&str(b.text))text+=b.text; }\nif(resp.stop_reason==='refusal'||!str(text)){\n  return [{ json: Object.assign({}, rep, { llm_status:'fallback_deterministic', llm_model:str(cfg.llm_model)||'claude-sonnet-4-6', llm_input_tokens:Number((resp.usage||{}).input_tokens)||0, llm_output_tokens:Number((resp.usage||{}).output_tokens)||0, llm_cost_usd:0, llm_summary_ru:'', llm_recommendations_ru:'', llm_quality_flags:'refused_or_empty; fell_back_to_deterministic', notes:str(rep.notes)+' || Claude summary refused/empty (stop_reason='+str(resp.stop_reason)+') \u2014 deterministic report kept.' }) }];\n}\nconst u=resp.usage||{};\nconst inTok=Number(u.input_tokens)||0, outTok=Number(u.output_tokens)||0;\nconst cost=Math.round(((inTok*(Number(cfg.llm_input_price_per_mtok)||3.0)+outTok*(Number(cfg.llm_output_price_per_mtok)||15.0))/1000000)*10000)/10000;\n// --- parse the required JSON sections\nconst flags=[];\nlet parsed=null;\ntry{ parsed=JSON.parse(str(text).replace(/^```(json)?/,'').replace(/```$/,'').trim()); }catch(e){ parsed=null; }\nif(resp.stop_reason==='max_tokens')flags.push('truncated_max_tokens');\nlet summaryRu='', recsRu='';\nif(parsed&&typeof parsed==='object'){\n  const kf=arr(parsed.key_findings), mr=arr(parsed.market_risks), na=arr(parsed.recommended_next_actions), cr=arr(parsed.content_recommendations), sl=arr(parsed.source_limitations);\n  summaryRu=[str(parsed.executive_summary_ru), kf.length?('\u041a\u043b\u044e\u0447\u0435\u0432\u044b\u0435 \u0432\u044b\u0432\u043e\u0434\u044b: '+kf.join(' \u00b7 ')):'', mr.length?('\u0420\u0438\u0441\u043a\u0438: '+mr.join(' \u00b7 ')):'', sl.length?('\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432: '+sl.join(' \u00b7 ')):''].filter(Boolean).join('\\n');\n  recsRu=[na.length?('\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f: '+na.join(' \u00b7 ')):'', cr.length?('\u041a\u043e\u043d\u0442\u0435\u043d\u0442: '+cr.join(' \u00b7 ')):''].filter(Boolean).join('\\n');\n  if(!str(parsed.executive_summary_ru))flags.push('missing_executive_summary');\n  if(!kf.length)flags.push('missing_key_findings');\n  // RU-NARRATIVE guard (MVP-F): a summary with no Cyrillic is not a Russian report narrative \u2014 treat it as\n  // invalid and keep the deterministic facts (same fallback as non-JSON output; nothing malformed persists).\n  if(str(parsed.executive_summary_ru)&&!/[\u0430-\u044f\u0451]/i.test(str(parsed.executive_summary_ru))){\n    return [{ json: Object.assign({}, rep, { llm_status:'fallback_deterministic', llm_model:str(cfg.llm_model)||'claude-sonnet-4-6', llm_input_tokens:inTok, llm_output_tokens:outTok, llm_cost_usd:cost, llm_summary_ru:'', llm_recommendations_ru:'', llm_quality_flags:'non_russian_summary; fell_back_to_deterministic', notes:str(rep.notes)+' || Claude summary not in Russian \u2014 fell back to the deterministic report.' }) }];\n  }\n}else{\n  // \u00a73 invalid (non-JSON) summary -> fall back to the deterministic report (facts unchanged). Claude never\n  // overrides counts/prices/competitors/evidence/source status; on any parse failure we keep the deterministic facts.\n  return [{ json: Object.assign({}, rep, { llm_status:'fallback_deterministic', llm_model:str(cfg.llm_model)||'claude-sonnet-4-6', llm_input_tokens:inTok, llm_output_tokens:outTok, llm_cost_usd:cost, llm_summary_ru:'', llm_recommendations_ru:'', llm_quality_flags:'non_json_output; fell_back_to_deterministic', notes:str(rep.notes)+' || Claude summary invalid (non-JSON) \u2014 fell back to the deterministic report.' }) }];\n}\n// --- anti-hallucination check: numbers in the output that are absent from the fact payload get flagged.\nconst factBlob=str(prompt.llm_facts_string);\nconst outNums=(summaryRu+' '+recsRu).match(/\\d{2,}/g)||[];\nif(outNums.some(n=>factBlob.indexOf(n)<0))flags.push('unverified_numbers_check_manually');\n// --- outreach guard: flag if the model slipped into outreach language despite the system prompt.\nif(/\u0430\u0443\u0442\u0440\u0438\u0447|\u0440\u0430\u0441\u0441\u044b\u043b\u043a|\u043e\u0431\u0437\u0432\u043e\u043d|\u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0435\u043c\u0443|\u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f \u0441 \u0430\u0432\u0442\u043e\u0440\u043e\u043c/i.test(summaryRu+' '+recsRu))flags.push('outreach_language_detected_review');\nconst llmStatus=flags.length?'ok_with_flags':'ok';\nreturn [{ json: Object.assign({}, rep, {\n  llm_status:llmStatus,\n  llm_model:str(cfg.llm_model)||'claude-sonnet-4-6',\n  llm_input_tokens:inTok,\n  llm_output_tokens:outTok,\n  llm_cost_usd:cost,\n  llm_summary_ru:summaryRu.slice(0,4000),\n  llm_recommendations_ru:recsRu.slice(0,2000),\n  llm_quality_flags:flags.join(', '),\n  notes:str(rep.notes)+' || llm_summary added (model='+(str(cfg.llm_model)||'claude-sonnet-4-6')+', input_tokens='+inTok+', output_tokens='+outTok+', cost_usd='+cost+', flags='+(flags.join('/')||'none')+'); evidence-bound prompt + budget guard (DEC-128), no invented facts/contacts/outreach.'\n}) }];"
      },
      "id": "wf12-llm-merge",
      "name": "Merge Claude Summary Into Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        180
      ]
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r5",
      "name": "Read competitor_site_snapshots",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        770,
        320
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "competitor_site_snapshots",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "notes": "v0.3 (DEC-128): tab may not exist yet \u2014 onError continue keeps the deterministic report working without it.",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r6",
      "name": "Read public_lead_signals",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        880,
        200
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "public_lead_signals",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "notes": "v0.3 (DEC-128): tab may not exist yet \u2014 onError continue keeps the deterministic report working without it.",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Build ONE live_source_runs row for the report layer (23 columns, DEC-126/128): logs whether the run\n// was deterministic ($0) or used the approved Claude branch, with token/cost accounting.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Report Config').first().json;\nlet rep=$('Build Deterministic Report').first().json;\ntry{ rep=Object.assign({},rep,$('Append market_intelligence_reports').first().json); }catch(e){}\nconst llmUsed=str(rep.llm_status)!==''&&str(rep.llm_status)!=='disabled';\nreturn [{ json: {\n  run_id:str(cfg.run_id),\n  created_at:str(cfg.created_at),\n  workflow:'WF12 - Market Intelligence Report Builder',\n  source_family:'report_layer',\n  platform:'sheets',\n  mode:llmUsed?'llm_summary':'deterministic',\n  approval_token_used:llmUsed?'yes':'no',\n  source_allowlist:'',\n  max_items:0,\n  items_received:Number(rep.rows_after_filters)||0,\n  items_relevant:Number(rep.rows_after_filters)||0,\n  items_written_raw:1,\n  items_unique:1,\n  items_duplicate:0,\n  hard_skipped:0,\n  external_calls:llmUsed?1:0,\n  estimated_source_cost_usd:0,\n  llm_calls:llmUsed?1:0,\n  estimated_llm_cost_usd:Number(rep.llm_cost_usd)||0,\n  status:'completed',\n  error_summary:'',\n  operator_next_action:(str(rep.report_type)==='no_data_notice')?'no_data; broaden filters/sources, re-run WF10 then WF12.':'Review the report row'+(llmUsed?' incl. llm_summary_ru / llm_quality_flags; record cost in COSTS_AND_LIMITS.md.':'.'),\n  notes:'wf12 v0.3 report run; report_id='+str(rep.report_id)+'; llm_status='+(str(rep.llm_status)||'disabled')+'.'\n}}];"
      },
      "id": "wf12-runlog-build",
      "name": "Build live_source_runs Row",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        560
      ]
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "live_source_runs",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "id": "wf12-app-runlog",
      "name": "Append live_source_runs",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        2200,
        560
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "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": "cc120000-0000-0000-0000-0000000000sh",
      "name": "Read source_health",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        880,
        80
      ],
      "alwaysOutputData": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "authentication": "serviceAccount",
        "documentId": {
          "__rl": true,
          "value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "raw_market_records",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "id": "cc120000-0000-0000-0000-0000000000rm",
      "name": "Read raw_market_records",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1060,
        80
      ],
      "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": "report_type",
              "type": "string"
            },
            {
              "name": "niche_id",
              "type": "string"
            },
            {
              "name": "region",
              "type": "string"
            },
            {
              "name": "top_n",
              "type": "string"
            },
            {
              "name": "enable_llm_summary",
              "type": "string"
            },
            {
              "name": "llm_approval_token",
              "type": "string"
            },
            {
              "name": "supplied_sources",
              "type": "string"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -440,
        520
      ],
      "id": "wf12-subtrig",
      "name": "When Called by Agent"
    },
    {
      "id": "a12-if-llmgate",
      "name": "Claude Gate Allowed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1500,
        180
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "a12-if-llmgate-c1",
              "leftValue": "={{ $json.llm_gate_allowed }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "a12-if-llmbudget",
      "name": "LLM Budget OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1720,
        180
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "a12-if-llmbudget-c1",
              "leftValue": "={{ !$json.llm_budget_blocked }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    }
  ],
  "connections": {
    "Manual Start": {
      "main": [
        [
          {
            "node": "Set Report Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Report Config": {
      "main": [
        [
          {
            "node": "Read competitor_profiles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read competitor_profiles": {
      "main": [
        [
          {
            "node": "Read market_angles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read market_angles": {
      "main": [
        [
          {
            "node": "Read audience_activity_signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read audience_activity_signals": {
      "main": [
        [
          {
            "node": "Read content_positioning_plan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read content_positioning_plan": {
      "main": [
        [
          {
            "node": "Read competitor_site_snapshots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Deterministic Report": {
      "main": [
        [
          {
            "node": "IF enable_llm_summary?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF enable_llm_summary?": {
      "main": [
        [
          {
            "node": "Claude Summary Approval Gate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Append market_intelligence_reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append market_intelligence_reports": {
      "main": [
        [
          {
            "node": "Build agent_requests Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build agent_requests Row": {
      "main": [
        [
          {
            "node": "Append agent_requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append agent_requests": {
      "main": [
        [
          {
            "node": "Build live_source_runs Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Summary Approval Gate": {
      "main": [
        [
          {
            "node": "Claude Gate Allowed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Claude Summary Prompt": {
      "main": [
        [
          {
            "node": "LLM Budget OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Claude Summary Into Report": {
      "main": [
        [
          {
            "node": "Append market_intelligence_reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read competitor_site_snapshots": {
      "main": [
        [
          {
            "node": "Read public_lead_signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read public_lead_signals": {
      "main": [
        [
          {
            "node": "Read source_health",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build live_source_runs Row": {
      "main": [
        [
          {
            "node": "Append live_source_runs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append live_source_runs": {
      "main": [
        [
          {
            "node": "Final Summary Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read source_health": {
      "main": [
        [
          {
            "node": "Read raw_market_records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Summary API Request": {
      "main": [
        [
          {
            "node": "Merge Claude Summary Into Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Called by Agent": {
      "main": [
        [
          {
            "node": "Set Report Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Gate Allowed?": {
      "main": [
        [
          {
            "node": "Build Claude Summary Prompt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Append market_intelligence_reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM Budget OK?": {
      "main": [
        [
          {
            "node": "Claude Summary API Request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Append market_intelligence_reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read raw_market_records": {
      "main": [
        [
          {
            "node": "Build Deterministic Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "wf12-v031-lead-block-stage35-20260617",
  "tags": []
}