{
  "schema_version": "1.0.0",
  "bot_id": "6.16",
  "bot_name": "ExposureExplainer",
  "slug": "exposure_explainer",
  "layer": "Governance",
  "layer_key": "gov",
  "bot_class": "Governance Service",
  "authority": [
    "Explain"
  ],
  "status": "planned",
  "readiness": "Spec ready",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Governance",
    "bot_class": "Governance",
    "authority": "Explain",
    "runs_before": "\u2014",
    "runs_after": "exec.order_lifecycle_manager",
    "applies_to": "Continuous",
    "default_mode": "shadow",
    "user_visible": "Yes",
    "developer_owner": "Governance pod"
  },
  "purpose": "Produces a plain-English narrative of current portfolio exposure: which markets, which outcomes, how much pUSD is at risk, why each position was opened (linked back to the originating strategy and OrderIntent). Read by the Admin UI, the daily ops digest, and any incident review.",
  "why_it_matters": [
    {
      "failure": "Opaque positions",
      "consequence": "A page of raw outcome IDs and notionals does not tell anyone whether the portfolio is balanced, concentrated, or running through resolution."
    },
    {
      "failure": "Audit trail",
      "consequence": "When something goes wrong, the first question is always 'why was that position open?' \u2014 without ExposureExplainer, the answer requires correlating five logs by hand."
    },
    {
      "failure": "Daily review fatigue",
      "consequence": "A narrative rendering compresses 200 line items into the 5 things a reviewer actually needs to read."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Resolved outcome positions",
      "source": "On-chain CTFExchangeV2 + PortfolioGuard",
      "required": true,
      "use": "Quantities by token."
    },
    {
      "input": "Market metadata",
      "source": "Polymarket REST",
      "required": true,
      "use": "Friendly market names and outcome labels."
    }
  ],
  "internal_inputs": [
    {
      "input": "Trade history",
      "source": "exec.order_lifecycle_manager",
      "required": true,
      "use": "Originating intent_id and strategy_id per fill."
    },
    {
      "input": "Strategy registry",
      "source": "Config",
      "required": true,
      "use": "Maps strategy_id to friendly description."
    }
  ],
  "raw_params": [
    "digest_cadence_minutes \u00b7 5\u20131440",
    "concentration_warn_pct \u00b7 5\u2013100"
  ],
  "parameters": [
    {
      "name": "digest_cadence_minutes",
      "default": 60,
      "warning": "\u2014",
      "hard": "\u2014",
      "controls": "How often a fresh narrative is emitted to the ops feed.",
      "why_default_matters": "Hourly is the right grain for trading hours; daily for overnight.",
      "threshold_logic": [
        {
          "condition": "60",
          "action": "Default"
        }
      ],
      "dev_check": "schedule.every(p.digest_cadence_minutes).do(emit_narrative);",
      "user_facing": "(Internal \u2014 not user-facing.)"
    },
    {
      "name": "concentration_warn_pct",
      "default": 25,
      "warning": 15,
      "hard": 25,
      "controls": "Single-market exposure (% of bankroll) at which the narrative flags concentration risk.",
      "why_default_matters": "25% in one market is significant; flagging keeps reviewers honest.",
      "threshold_logic": [
        {
          "condition": "\u2264 15%",
          "action": "Silent"
        },
        {
          "condition": "15\u201325%",
          "action": "MENTION"
        },
        {
          "condition": "> 25%",
          "action": "FLAG"
        }
      ],
      "dev_check": "if (mkt.exposure_pct > p.concentration_warn_pct) flag(mkt);",
      "user_facing": "(Internal \u2014 not user-facing.)"
    }
  ],
  "default_config": {
    "digest_cadence_minutes": 60,
    "concentration_warn_pct": 25
  },
  "flow": "Aggregate positions and trade history \u2192 join against market + strategy registry \u2192 bucket by (strategy, market, outcome) \u2192 render Markdown narrative with concentration highlights \u2192 emit OperationsReport(kind=ExposureNarrative).",
  "decision_logic": {
    "approve": "Group by strategy \u2192 market \u2192 outcome. Annotate each row with originating intent and decision rationale.",
    "reshape_required": "This bot does not reshape orders.",
    "reject": "No reject path defined for this bot \u2014 it is observe-only.",
    "warning_only": "Highlight concentration above warn threshold."
  },
  "decision_output_example": {
    "kind": "ExposureNarrative",
    "summary_markdown": "## Exposure as of 14:00 UTC\n- Strategy 'event_drift' is long $1,200 on YES in market 0xabc (Trump Q1 announcement); concentration 18%.\n- Strategy 'late_unwind' is short $400 on NO in market 0xdef.\n",
    "concentration_flags": []
  },
  "developer_log": "Per emission: digest_id, ts_ms, position_count, total_exposure_usd, flagged_markets.",
  "user_explanations": [
    {
      "situation": "When this bot acts",
      "message": "Hourly summary of every open position the system holds, in plain language."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Stale strategy registry produces narratives that label positions with the wrong strategy.",
    "false_positive_risk": "Concentration flag fires for a position that is intentionally large; mitigation: allow per-strategy whitelist with required reviewer notes.",
    "false_negative_risk": "On-chain position is held outside the standard portfolio path and is missed; mitigation: reconcile against on-chain CTFExchangeV2 token balances every digest.",
    "safe_fallback": "On any data fetch failure, emit a narrative containing only the failure summary plus the last successful narrative's timestamp. Never produce a misleading narrative."
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Concentration > 25% flags the market.",
        "setup": "Synthetic fixture per template.",
        "expected": "Behaviour matches the rule described in the test name."
      },
      {
        "test": "Empty portfolio renders 'No open positions.'.",
        "setup": "Synthetic fixture per template.",
        "expected": "Behaviour matches the rule described in the test name."
      }
    ],
    "integration": [
      {
        "test": "Run against a recorded portfolio snapshot; the rendered narrative matches the golden file.",
        "expected": "End-to-end behaviour matches the spec without manual intervention."
      }
    ],
    "property": [
      {
        "property": "Narrative line count is bounded by position_count + flagged_markets + 5.",
        "required": "Always true across all generated inputs."
      }
    ]
  },
  "reference_implementation": {
    "language": "pseudocode",
    "pseudocode": "positions = portfolio.snapshot()\nrows = [(p.strategy_id, p.market_id, p.outcome_id, p.size_usd) for p in positions]\nrows.sort(key=lambda r: -r[3])\nfor r in rows:\n  narrative.append(render_row(r))\n  if exposure_pct(r) > p.concentration_warn_pct: flags.append(r.market_id)\nemit('ExposureNarrative', narrative, flags)"
  },
  "wire_examples": {
    "input": {
      "snapshot_ts_ms": 1715260000000,
      "positions": [
        {
          "strategy_id": "event_drift",
          "market_id": "0xabc",
          "outcome_id": "YES",
          "size_usd": 1200
        }
      ]
    },
    "output": {
      "kind": "ExposureNarrative",
      "summary_markdown": "## Exposure as of 14:00 UTC\n- Strategy 'event_drift' is long $1,200 on YES in market 0xabc.\n",
      "concentration_flags": []
    }
  },
  "reason_codes": [
    {
      "code": "GOV_EXPOSURE_NARRATIVE",
      "severity": "P3",
      "meaning": "Gov Exposure Narrative",
      "action": "See decision output and developer log for context.",
      "user_message": "Hourly summary of every open position the system holds, in plain language."
    },
    {
      "code": "GOV_EXPOSURE_CONCENTRATION_FLAG",
      "severity": "P3",
      "meaning": "Gov Exposure Concentration Flag",
      "action": "See decision output and developer log for context.",
      "user_message": "Hourly summary of every open position the system holds, in plain language."
    },
    {
      "code": "GOV_EXPOSURE_FALLBACK",
      "severity": "P3",
      "meaning": "Gov Exposure Fallback",
      "action": "See decision output and developer log for context.",
      "user_message": "Hourly summary of every open position the system holds, in plain language."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "narratives_emitted_total",
        "type": "counter",
        "unit": "event",
        "labels": [
          "bot_id"
        ],
        "meaning": "Narratives emitted total."
      },
      {
        "name": "concentration_flags_total",
        "type": "counter",
        "unit": "event",
        "labels": [
          "bot_id"
        ],
        "meaning": "Concentration flags total."
      },
      {
        "name": "narrative_render_failures_total",
        "type": "counter",
        "unit": "event",
        "labels": [
          "bot_id"
        ],
        "meaning": "Narrative render failures total."
      }
    ],
    "alerts": [],
    "dashboards": [
      "6.16 overview dashboard"
    ]
  },
  "state": {
    "summary": "Last successful narrative cached for fallback emission.",
    "stores": [
      {
        "name": "exposure_explainer_state",
        "kind": "in-memory + fast KV mirror",
        "key": "bot_id",
        "value": "Last successful narrative cached for fallback emission.",
        "ttl": "24h",
        "durability": "crash-safe via KV mirror"
      }
    ],
    "recovery": "Cold-start hydrates from fast KV; missing keys default to safe fallback.",
    "on_restart": "All in-flight decisions are re-evaluated; no bot decision is trusted across restart without re-emit."
  },
  "concurrency": {
    "execution_model": "Single emitter. Reads upstream snapshots only.",
    "max_in_flight": 32,
    "idempotency_key": "order_intent_id",
    "replay_safe": true,
    "deduplication": "By idempotency_key within a 60s window.",
    "ordering_guarantees": "Per-market_id FIFO; cross-market unordered.",
    "timeout_ms": 250,
    "backpressure": "Bounded queue; oldest-dropped with metric increment when full.",
    "locking": "Per-market_id mutex; no global locks."
  },
  "dependencies": {
    "depends_on": [
      "exec.order_lifecycle_manager"
    ],
    "emits_to": []
  },
  "graph": {
    "requires": [
      "exec.order_lifecycle_manager"
    ],
    "required_before": [],
    "consumes": [
      "PortfolioSnapshot",
      "TradeHistory"
    ],
    "emits": [
      "OperationsReport(kind=ExposureNarrative)"
    ],
    "blocks": false
  },
  "mode_support": [
    "off",
    "shadow",
    "advisory",
    "enforced"
  ],
  "latency_budget_ms": 1500,
  "data_freshness": {
    "max_market_data_age_ms": 60000,
    "max_orderbook_age_ms": 60000,
    "on_stale_data": "Emit fallback narrative; do not render misleading data."
  },
  "ownership": {
    "owner": "Governance pod",
    "on_call": "gov-oncall",
    "channel": "#polytraders-gov",
    "escalation": "Head of Governance",
    "severity_class": "P3"
  },
  "human_override": {
    "allowed": true,
    "who": "Governance on-call",
    "log_event": "GOV_EXPOSURE_OVERRIDE",
    "time_bound": "Single emission",
    "scope": "Single narrative",
    "second_approval": false
  },
  "security_surfaces": {
    "summary": "Read-only on portfolio + history. Emits to internal ops feed only.",
    "signing": "None \u2014 bot does not sign or submit.",
    "secrets": [],
    "contract_calls": [],
    "abuse_vectors": [],
    "mitigations": [
      "Rate-limit per source",
      "Audit-log every override",
      "Require role-based authz on admin paths"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "V2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": true,
    "multichain_ready": true,
    "sdk_used": "Polymarket CLOB V2 SDK",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Reads V2 token balances and lifecycle state."
  },
  "version": {
    "current": "0.1.0",
    "contract_version": "1.0.0",
    "last_breaking_change": "none",
    "deprecation_window_days": 30
  },
  "migration_history": [],
  "runbook": {
    "summary": "If narratives stop emitting: check the digest scheduler heartbeat in monitoring.",
    "oncall_actions": [
      {
        "alert": "6.16_anomaly",
        "first_step": "Open the bot's reporting page and confirm the alert is real (not a metric hiccup).",
        "diagnosis": "Inspect developer log entries for the affected market_id over the last 30 minutes.",
        "mitigation": "Force-clear via Admin UI if the rule is clearly stale; otherwise leave engaged and notify owner.",
        "escalation": "Governance pod"
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders bot pause 6.16",
        "effect": "Disables the bot's enforcement layer; downstream consumers fall back to safe defaults."
      }
    ],
    "healthcheck": "GET /healthz/exposure_explainer \u2192 200 if last successful evaluation < 60s ago."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Stub",
        "how_measured": "golden-file diff.",
        "threshold": "Documented threshold met for the full window."
      }
    ],
    "to_limited_live": [
      {
        "gate": "Shadow",
        "how_measured": "14 days; narratives reviewed by ops.",
        "threshold": "Documented threshold met for the full window."
      },
      {
        "gate": "Advisory",
        "how_measured": "7 days.",
        "threshold": "Documented threshold met for the full window."
      }
    ],
    "to_general_live": [
      {
        "gate": "Enforced",
        "how_measured": "feeds the daily ops digest.",
        "threshold": "Documented threshold met for the full window."
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "Drop the strategy registry and assert fallback narrative renders",
      "how_to_inject": "Drop the strategy registry and assert fallback narrative renders.",
      "expected_behavior": "Bot detects within its latency budget and emits the corresponding reason code.",
      "recovery": "Remove the injected fault; bot returns to healthy state within one debounce window."
    },
    {
      "scenario": "Inject a position with no originating intent and assert it is flagged as ORPHAN",
      "how_to_inject": "Inject a position with no originating intent and assert it is flagged as ORPHAN.",
      "expected_behavior": "Bot detects within its latency budget and emits the corresponding reason code.",
      "recovery": "Remove the injected fault; bot returns to healthy state within one debounce window."
    }
  ],
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 7,
    "phase_name": "Governance & replay",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}