{
  "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();\nreturn [{ json: {\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:false,\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}}];"
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r1",
      "name": "Read competitor_profiles",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        0,
        320
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "competitor_profiles",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r2",
      "name": "Read market_angles",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        220,
        320
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "market_angles",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r3",
      "name": "Read audience_activity_signals",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        440,
        320
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "audience_activity_signals",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r4",
      "name": "Read content_positioning_plan",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        660,
        320
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "content_positioning_plan",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000b1",
      "name": "Build Deterministic Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        320
      ],
      "parameters": {
        "jsCode": "// --- 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\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  if (review_status === 'pending' || /(^|; )pending_review/.test(flags)) 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  const id = str(row.source_run_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  const selfAttestsLive = (dm === 'live' || dm === 'production') &&\n    (qs === '' || qs === 'healthy' || (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 || 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// 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 leadSignals=rowsOf('Read public_lead_signals');\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;\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\nlet profNow=profiles.filter(p=>str(p.notes).indexOf('wf10_'+stamp)>=0||str(p.notes).indexOf(stamp)>=0);\nlet angNow=angles.filter(a=>str(a.angle_id).endsWith(stamp));\nconst angPrev=prevStamp?angles.filter(a=>str(a.angle_id).endsWith(prevStamp)):[];\nlet sigNow=signals.filter(s=>str(s.signal_id).endsWith(stamp));\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// --- 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=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(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);\nconst topComp=topCompRows.map(p=>cleanName(p)+' (evidence '+num(p.evidence_count)+', conf '+num(p.source_confidence_score)+')');\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);});\nconst audSummary=sigNow.map(s=>str(s.platform)+': questions='+num(s.question_count)+', objections='+num(s.objection_count)+', complaints='+num(s.complaint_count)+', buying_intent='+num(s.buying_intent_count)+(str(s.top_pains)?(', pains: '+str(s.top_pains)):'')).join(' | ');\nconst NO_DATA_TEXT='no_data; broaden filters or source scope';\n// --- v0.3 shortened offers/prices\nconst offersPrices=profNow.filter(p=>str(p.offers)||str(p.prices_terms)).slice(0,5)\n  .map(p=>cleanName(p)+': '+cut(str(p.offers).split(' | ')[0]||'(\u043e\u0444\u0444\u0435\u0440 \u043d\u0435 \u0438\u0437\u0432\u043b\u0435\u0447\u0451\u043d)',maxOffer)+(str(p.prices_terms)?(' \u2014 '+cut(str(p.prices_terms).split(' | ')[0],maxOffer)):''));\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;\nconst confLine=confScores.length?('avg='+confAvg+', min='+Math.min.apply(null,confScores)+', max='+Math.max.apply(null,confScores)+' (\u043f\u043e '+confScores.length+' \u043f\u0440\u043e\u0444\u0438\u043b\u044f\u043c; \u043f\u0440\u0430\u0432\u0438\u043b\u0430: source_confidence_rules)'):'(\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;}\nconst byDomain={};\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)){website_snapshots_excluded++;continue;}\n  if(!byDomain[d]||str(s.created_at)>str(byDomain[d].created_at))byDomain[d]=s;}\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(str(s.offer_summary)||'(offer n/a)',maxOffer)\n   +(str(s.prices_terms)?(' \u00b7 \u0446\u0435\u043d\u044b: '+cut(s.prices_terms,60)):'')\n   +(str(s.guarantees)?(' \u00b7 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438: '+cut(s.guarantees,60)):'')\n   +(str(s.cta_text)?(' \u00b7 CTA: '+cut(s.cta_text,50)):'')\n   +(str(s.change_type)&&low(s.change_type)!=='baseline'?(' \u00b7 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435: '+str(s.change_type)):'');});\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// --- 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?('public_lead_signals: '+activeLeads.length+' (new: '+newLeads+') \u00b7 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 H/M/L: '+hiLeads+'/'+medLeads+'/'+loLeads+' \u00b7 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e '+contacts_detected+' / \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0438\u0437 \u043e\u0442\u0447\u0451\u0442\u0430 '+contacts_excluded_from_report+' / \u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043e '+contacts_redacted+' (report_contains_contacts=false) \u00b7 intents: '+intentLine+' \u00b7 top pains: '+topLeadPains+' \u00b7 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0430: manual_review/aggregate_only, \u0411\u0415\u0417 \u0430\u0443\u0442\u0440\u0438\u0447\u0430'):(leadSignals.length?('public_lead_signals: '+leadSignals.length+' \u0441\u0442\u0440\u043e\u043a, \u043d\u043e \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u043d\u0435\u0442 \u2014 \u043d\u0435\u0447\u0435\u0433\u043e \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u044c'):'(\u0432\u043a\u043b\u0430\u0434\u043a\u0430 public_lead_signals \u043f\u0443\u0441\u0442\u0430 \u2014 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c WF14 Lead Scout triage)');\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)||'manual_review')+(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 (\u0441\u043c. \u0441\u0442\u0440\u043e\u043a\u0443, manual_review)':'')+'\\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('\u041a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0432 \u043f\u043e\u043b\u0435 \u0437\u0440\u0435\u043d\u0438\u044f: '+profNow.length+'; \u0441\u0430\u043c\u044b\u0439 \u0437\u0430\u043c\u0435\u0442\u043d\u044b\u0439 \u2014 '+(topCompRows.length?cleanName(topCompRows[0]):'(\u043d\u0435\u0442)')+'.');\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')+'; \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0441\u043c\u0435\u0448\u0430\u043d\u043d\u044b\u0435 (live + \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435 + \u0432\u0435\u0431).');\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\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c WF10 + WF12.']:[\n  (activeLeads.length?('\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c '+newLeads+' \u043d\u043e\u0432\u044b\u0445 \u0441\u0442\u0440\u043e\u043a public_lead_signals (review_status=new, \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442=high): set accepted/rejected/duplicate/stale. \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u2014 \u0442\u043e\u043b\u044c\u043a\u043e manual_review, \u0411\u0415\u0417 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439; outreach_allowed=false.'):'\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c WF14 (Lead Scout triage), \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c public_lead_signals.'),\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 \u0443\u0433\u043b\u044b (competitor_profiles / market_angles): \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.'):'\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c competitor_site_snapshots: \u043f\u0440\u043e\u0433\u043d\u0430\u0442\u044c WF04 \u043f\u043e \u0441\u0430\u0439\u0442\u0430\u043c \u0442\u043e\u043f-\u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (\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);\nconst sourceActions=isNoData?['\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043a\u043e\u043d\u043d\u0435\u043a\u0442\u043e\u0440\u044b (WF09/WF11/WF13) \u0438 WF08, \u0437\u0430\u0442\u0435\u043c WF10.']:[\n  'Avito (WF09): live \u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u2014 \u043f\u043b\u0430\u043d\u043e\u0432\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d \u043f\u043e \u043b\u0438\u043c\u0438\u0442\u0430\u043c \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 (COSTS_AND_LIMITS).',\n  'Telegram (WF11): \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043f\u0440\u0435\u0432\u044c\u044e \u043f\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u043c \u043a\u0430\u043d\u0430\u043b\u0430\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d \u2014 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0447\u0435\u0440\u0435\u0437 approval gate, \u043b\u0438\u043c\u0438\u0442\u044b \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u0438 \u0444\u0438\u043b\u044c\u0442\u0440 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u043e\u0441\u0442\u0438. \u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u2014 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c/\u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\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 \u0438 \u043e\u0434\u043e\u0431\u0440\u0438\u0442\u044c \u0437\u0430\u043f\u0443\u0441\u043a (\u0431\u0435\u0437 \u0430\u0443\u0442\u0440\u0438\u0447\u0430).',\n  'VK (WF13): \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 VK \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e \u043e\u0442\u0447\u0451\u0442\u0430 C.1, \u043d\u043e live-smoke C.4 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d \u0434\u043e \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f Stage C/MVP. \u0422\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f \u0438 VK-\u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b\u0430 (\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API/HTTP-\u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442).',\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 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u044b\u043c \u043f\u0440\u043e\u0433\u043e\u043d\u043e\u043c \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u043e\u0432 Stage 2 (WF04/WF06, \u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).':'\u0421\u0430\u0439\u0442\u044b: competitor_site_snapshots \u0435\u0449\u0451 \u043d\u0435 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d \u2014 \u043f\u043e\u0441\u043b\u0435 Stage 2 cleanup \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u043e\u0432 \u043f\u043e 3\u20135 \u0442\u043e\u043f-\u0441\u0430\u0439\u0442\u0430\u043c \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (WF04/WF06, \u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).')];\nfunction ol(arr){return arr.map((x,i)=>(i+1)+'. '+x).join('\\n')||'(\u043d\u0435\u0442)';}\n// --- mandatory source-mix label (DEC-108)\nconst SOURCE_MIX='mixed: live + historical/manual + web pipeline';\nconst reportType=isNoData?'no_data_notice':str(cfg.report_type);\n// --- deterministic Markdown render (v0.3 stakeholder report)\nconst md=[\n'# Market intelligence report '+str(cfg.report_id)+' (v0.3)',\n(isFixtureReport?'> **TEST / FIXTURE DATA \u2014 NOT PRODUCTION INTELLIGENCE**':''),\n(quality_warning?('> **QUALITY WARNING:** '+quality_warning):''),\n'',\n'- niche: '+str(cfg.niche_id)+' \u00b7 region: '+str(cfg.region)+' \u00b7 WF10 run: wf10_'+stamp+(prevStamp?(' \u00b7 prev: wf10_'+prevStamp):' \u00b7 prev: (\u043d\u0435\u0442 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u043f\u0440\u043e\u0433\u043e\u043d\u0430 \u2014 trend_status=no_compatible_baseline)'),\n'- **\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438: '+SOURCE_MIX+'** \u00b7 rows_after_filters='+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 (executive_digest)',digestMd,\n'',\nisNoData?('**NO DATA:** '+NO_DATA_TEXT):[\n'## \u0421\u043d\u0430\u043f\u0448\u043e\u0442 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (competitor_snapshot)','\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'):'(none in snapshot)',\n'','## \u0422\u043e\u043f \u043e\u0444\u0444\u0435\u0440\u043e\u0432 \u0438 \u0446\u0435\u043d (top_offers_and_prices)',offersPrices.length?offersPrices.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 (competitor_websites \u2014 Stage 2 reintegration)',siteLines.length?siteLines.map((s,i)=>(i+1)+'. '+s).join('\\n'):'(competitor_site_snapshots \u043f\u0443\u0441\u0442 \u2014 \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442, \u0447\u0442\u043e \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u044b Stage 2 \u0435\u0449\u0451 \u043d\u0435 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u044b, \u0430 \u043d\u0435 \u0441\u0431\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b; \u043f\u043e\u0441\u043b\u0435 Stage 2 cleanup \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d WF04/WF06 \u2014 \u0441\u043c. \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u043c)',\nwebProfiles.length?('\u041f\u0440\u043e\u0444\u0438\u043b\u0438 WF10 \u0441 \u0432\u0435\u0431-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c: '+webProfiles.map(p=>cleanName(p)).join('; ')):'',\n'','## \u0422\u043e\u043f \u0443\u0433\u043b\u043e\u0432 (market_angles_summary)',topAng.length?topAng.map((a,i)=>(i+1)+'. '+a).join('\\n'):'(none in snapshot)',\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 (aggregate-only)',(audSummary||'(no audience signals in snapshot)'),'',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; outreach_allowed=false; \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'','## \u041f\u043b\u0430\u043d \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430','plan: '+str(latest.plan_id)+' \u2014 '+str(latest.top_angles),\n'','## \u0414\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u043d\u043e\u0441\u0442\u044c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432 (source_confidence)',confLine\n].join('\\n'),\n'',\n'## \u041a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432 (source_quality \u2014 WF16)',\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 \u043a \u043e\u0442\u0447\u0451\u0442\u0443: '+included_source_runs.length+' \u00b7 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043e: '+excluded_source_runs.length+(excluded_source_runs.length?(' ('+excluded_source_runs.join(', ')+')'):''),\n'- report_contains_degraded_data='+report_contains_degraded_data+' \u00b7 report_contains_contacts=false \u00b7 trend_status='+trend_status+(website_snapshots_excluded?(' \u00b7 degraded website snapshots excluded: '+website_snapshots_excluded):'')+(body_records_excluded?(' \u00b7 ineligible body records excluded: '+body_records_excluded):''),\n'',\n'## \u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f \u0438 source mix (limitations)',\n'- '+SOURCE_MIX+' \u2014 \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0442 \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435/\u0440\u0443\u0447\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438, \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0436\u0438\u0432\u043e\u0439 \u0441\u0431\u043e\u0440 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u043f\u0440\u043e\u0433\u043e\u043d\u0430.',\n'- \u0422\u0440\u0435\u043d\u0434\u044b: '+(hasBaseline?('\u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0435 \u0441 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u044b\u043c \u043f\u0440\u043e\u0433\u043e\u043d\u043e\u043c wf10_'+prevStamp):'\u043d\u0435\u0442 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u043f\u0440\u043e\u0433\u043e\u043d\u0430 (trend_status=no_compatible_baseline) \u2014 \u043f\u043e\u0437\u0438\u0446\u0438\u0438 \u041d\u0415 \u043f\u043e\u043c\u0435\u0447\u0430\u044e\u0442\u0441\u044f \u043a\u0430\u043a NEW')+'.',\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 = evidence, \u041d\u0415 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u0430\u0443\u0442\u0440\u0438\u0447 (CONTACT_AND_OUTREACH_POLICY).',\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'## \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 (manager_review_actions)',ol(mgrActions),\n'',\n'## \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0443 (content_actions)',ol(contentActions),\n'',\n'## \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u043c (source_collection_actions)',ol(sourceActions),\n'',\n'_Deterministic report v0.3 (DEC-112/122/128): \u0444\u0430\u043a\u0442\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0437 \u0432\u043a\u043b\u0430\u0434\u043e\u043a; no LLM by default, no contacts, no outreach. $0._'\n].join('\\n');\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  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) \u2014 fires on enable_llm_summary=true. INERT BY DEFAULT:\n// throws unless the operator explicitly armed the run, and even then the downstream HTTP node ships\n// DISABLED with no credential \u2014 enabling it is a second deliberate operator action in the n8n editor.\n// Claude is the business-interpretation layer ON TOP of the deterministic WF10 fact core (DEC-112):\n// it may only summarize the deterministic report \u2014 no invented facts, no contacts, no outreach.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Report Config').first().json;\nif(str(cfg.llm_approval_token)!=='I_APPROVE_CLAUDE_REPORT_SUMMARY'){\n  throw new Error('WF12 Claude gate: operator approval token missing. The optional Claude summary requires: llm_approval_token=I_APPROVE_CLAUDE_REPORT_SUMMARY in Set Report Config, an Anthropic credential bound in n8n (never in this file), and ENABLING the disabled HTTP node. Deterministic report stays $0 by default (DEC-112). Cost must be recorded in agent_requests + docs/COSTS_AND_LIMITS.md.');\n}\nreturn $input.all();"
      }
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000a1",
      "name": "Append market_intelligence_reports",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1320,
        440
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "market_intelligence_reports",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "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": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "agent_requests",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "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  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  throw new Error('WF12 Claude budget guard (DEC-128): estimated cost $'+estCost+' exceeds llm_max_estimated_cost_usd=$'+maxCost+' (est input tokens='+estInputTokens+', max output tokens='+maxOut+'). Reduce llm_max_tokens / llm_max_input_chars or raise the budget consciously. No API call was made.');\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_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://api.anthropic.com/v1/messages",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            },
            {
              "name": "x-api-key",
              "value": "BIND_N8N_ANTHROPIC_CREDENTIAL_NEVER_PASTE_KEY"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.llm_request_body) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "wf12-llm-http-placeholder",
      "name": "Claude Summary API Request (DISABLED placeholder)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        180
      ],
      "disabled": true,
      "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."
    },
    {
      "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  throw new Error('WF12 Claude merge: no API response \u2014 the Claude HTTP placeholder node is DISABLED (inert by design, DEC-122). Enable it with a bound n8n credential only after operator approval; default runs write the deterministic report ($0).');\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)){ throw new Error('WF12 Claude merge: empty or refused summary (stop_reason='+str(resp.stop_reason)+') \u2014 keep the deterministic report; investigate before retrying.'); }\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}else{\n  flags.push('non_json_output');\n  summaryRu=str(text).slice(0,4000);\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": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "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."
    },
    {
      "id": "cc120000-0000-0000-0000-0000000000r6",
      "name": "Read public_lead_signals",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        880,
        200
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "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."
    },
    {
      "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": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "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
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "={{ $json.spreadsheet_id_placeholder || \"PASTE_SPREADSHEET_ID\" }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "source_health",
          "mode": "name"
        },
        "options": {}
      },
      "id": "cc120000-0000-0000-0000-0000000000sh",
      "name": "Read source_health",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        880,
        80
      ],
      "alwaysOutputData": true
    }
  ],
  "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": "Build Claude Summary Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Claude Summary Prompt": {
      "main": [
        [
          {
            "node": "Claude Summary API Request (DISABLED placeholder)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Summary API Request (DISABLED placeholder)": {
      "main": [
        [
          {
            "node": "Merge Claude Summary Into Report",
            "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": "Build Deterministic Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "wf12-v031-lead-block-stage35-20260617",
  "tags": []
}