{
  "schema_version": "1.0.0",
  "bot_id": "4.8",
  "bot_name": "ResolutionRuleParser",
  "slug": "resolutionruleparser",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": true,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Intelligence",
    "bot_class": "Signal Service",
    "authority": "Read-only",
    "runs_before": "",
    "runs_after": "",
    "applies_to": "",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core"
  },
  "purpose": "Convert each market's resolution rule into a structured representation: source-of-truth, condition, ambiguity score.",
  "why_it_matters": [
    {
      "failure": "Free-text rule treated as if structured",
      "consequence": "Resolution rules are written in English and parsed by humans. Without a structured representation, every downstream consumer \u2014 ContradictionDetector, RuleChangeMonitor, SourceOfTruthVerifier \u2014 re-parses the same string and gets subtly different answers.",
      "worked_example": {
        "setup": "Market `Will Bill X be signed by 31 Dec 2026?` has a one-paragraph rule citing 'the official White House press release'.",
        "without_bot": "Three downstream bots each parse the paragraph independently. ContradictionDetector concludes the source is `whitehouse.gov`. RuleChangeMonitor sees no change when the rule is later edited to `the official White House press release or comparable announcement`. SourceOfTruthVerifier never runs because it can't extract a URL.",
        "with_bot": "ResolutionRuleParser produces `{source: 'whitehouse.gov', condition: 'press_release_signed', deadline: '2026-12-31T23:59Z', ambiguity: 0.3}`. All three consumers read the same structured fact and the rule edit changes `ambiguity` to 0.6, which RuleChangeMonitor flags."
      }
    },
    {
      "failure": "Ambiguity score not surfaced to strategies",
      "consequence": "A resolution rule with high ambiguity carries non-trivial settlement risk. Without an explicit score, strategy bots cannot down-size or skip those markets."
    },
    {
      "failure": "Source-of-truth field unused",
      "consequence": "A structured 'source-of-truth' field lets SourceOfTruthVerifier check whether the cited source actually publishes the data needed to resolve. The parser is the upstream that makes that check possible."
    },
    {
      "failure": "Rule changes hard to diff",
      "consequence": "RuleChangeMonitor needs to detect post-listing edits. Diffing structured representations catches semantic changes; diffing raw strings flags every cosmetic edit and misses meaningful ones."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Market resolution_rules and resolution_source from Gamma API",
      "source": "gamma",
      "required": true,
      "use": "Primary source for extracting and hashing resolution rules."
    },
    {
      "input": "Market condition_id and question text",
      "source": "data",
      "required": true,
      "use": "Cross-reference condition_id to market metadata for rule provenance."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Suppress all ResolutionRuleParser emissions when KillSwitch is active."
    }
  ],
  "raw_params": [
    "min_confidence_to_publish \u00b7 0\u20131",
    "require_human_review \u00b7 list",
    "regression_alert_drop \u00b7 float",
    "republish_on_edit \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "poll_interval_s",
      "default": 300,
      "warning": 900,
      "hard": 3600,
      "controls": "Seconds between Gamma API polls for resolution rule updates per active market.",
      "why_default_matters": "300 s (5 min) provides timely detection of rule changes without overloading Gamma API.",
      "threshold_logic": [
        {
          "condition": "interval <= 300 s",
          "action": "Normal"
        },
        {
          "condition": "300\u2013900 s",
          "action": "WARN \u2014 reduced detection speed for rule changes"
        },
        {
          "condition": "> 3600 s",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.poll_interval_s > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Resolution rules are checked regularly to detect any market condition changes."
    },
    {
      "name": "staleness_threshold_s",
      "default": 600,
      "warning": 1200,
      "hard": 7200,
      "controls": "Seconds after which a cached Gamma response is considered stale.",
      "why_default_matters": "600 s matches twice the default poll interval, ensuring at least one retry before stale status.",
      "threshold_logic": [
        {
          "condition": "age <= 600 s",
          "action": "Normal"
        },
        {
          "condition": "600\u20131200 s",
          "action": "WARN \u2014 stale data approaching"
        },
        {
          "condition": "> 7200 s",
          "action": "Reject \u2014 emit STALE_DATA and suppress"
        }
      ],
      "dev_check": "if (cache_age_s > p.staleness_threshold_s.hard) emit('STALE_DATA');",
      "user_facing": "Cached resolution rules expire regularly to ensure freshness."
    }
  ],
  "default_config": {
    "bot_id": "intel.resolutionruleparser",
    "version": "0.1.0",
    "mode": "planned",
    "defaults": {
      "poll_interval_s": 300,
      "staleness_threshold_s": 600
    },
    "locked": {
      "poll_interval_s": {
        "max": 3600
      },
      "staleness_threshold_s": {
        "max": 7200
      }
    }
  },
  "implementation_flow": [],
  "decision_logic": {
    "approve": "",
    "reshape_required": "",
    "reject": "",
    "warning_only": ""
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "report_id": "rep_rrp_0xf1a2_1746703000000",
    "trace_id": "trc_0xbeef0102030405060709",
    "bot_id": "intel.resolutionruleparser",
    "kind": "ObservationReport",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "resolution_source": "UMA Optimistic Oracle",
    "resolution_rules_hash": "0xabcd1234",
    "oracle_bond_pusd": 750,
    "neg_risk": false,
    "change_detected": false,
    "emitted_at_ms": 1746703005000
  },
  "developer_log": {
    "bot_id": "intel.resolutionruleparser",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "rules_hash": "0xabcd1234",
    "change_detected": false,
    "gamma_response_ms": 120,
    "killswitch_active": false,
    "emitted_at_ms": 1746703005000
  },
  "user_explanations": [
    {
      "situation": "Strategy paused entry on a market after rule change detected",
      "message": "The resolution conditions for this market were recently updated. The system flagged this for review before allowing new positions."
    },
    {
      "situation": "Resolution source shows UMA Optimistic Oracle",
      "message": "This market resolves via UMA's decentralised oracle, which requires a $750 pUSD bond and a 2-hour challenge window."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Gamma API outage prevents resolution rule fetching, leaving strategy layer with stale or missing rule data for active markets.",
    "false_positive_risk": "Minor whitespace or formatting changes to resolution_rules text generate false rule-change alerts despite no substantive change.",
    "false_negative_risk": "Gamma API returns cached (stale) rules during a CMS update, causing a genuine rule change to be missed until next successful fresh fetch.",
    "safe_fallback": "If Gamma API is unavailable for > staleness_threshold_s, emit STALE_DATA WARN and suppress new ObservationReports until fresh data is available. Retain last known good rules hash.",
    "required_dependencies": [
      "Polymarket Gamma API",
      "KillSwitch active flag",
      "Postgres for rule snapshot storage"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Market with valid resolution rules emits ObservationReport",
        "setup": "Gamma returns market with resolution_rules and resolution_source populated",
        "expected": "ObservationReport emitted with rules_hash and source"
      },
      {
        "test": "Market with empty resolution rules skips emission and emits WARN",
        "setup": "Gamma returns market with resolution_rules=null",
        "expected": "RESOLUTIONRULEPARSER_MISSING_RULES WARN; no ObservationReport"
      },
      {
        "test": "KillSwitch suppresses emission",
        "setup": "killswitch.active=true; valid rules available",
        "expected": "No ObservationReport; KILL_SWITCH_ACTIVE logged"
      }
    ],
    "integration": [
      {
        "test": "Rule change detected and emitted correctly",
        "expected": "ObservationReport emitted with change_detected=true when rules_hash changes between polls"
      },
      {
        "test": "Gamma API outage: STALE_DATA emitted; last rules hash retained",
        "expected": "STALE_DATA WARN; no new ObservationReports; last rules hash preserved in Postgres"
      }
    ],
    "property": [
      {
        "property": "ResolutionRuleParser never submits or signs orders",
        "required": "Always true"
      },
      {
        "property": "No ObservationReport emitted when KillSwitch is active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Convert each market's resolution rule into a structured representation: source-of-truth, condition, ambiguity score.",
  "legacy_pm_signals": [
    "Market metadata: full rule text, source-of-truth field, neg-risk topology",
    "Versioned diffs when rules are edited post-listing",
    "Confidence and contradiction counts per market",
    "Cross-rule references (this market resolves only if X)"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "data",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q3-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Parses Gamma market resolution rules; references negRisk flag for multi-outcome markets. No order signing."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION parseResolutionRules(condition_id):\n  // 0. KillSwitch check\n  IF FETCH internal.killswitch.status == ACTIVE:\n    RETURN\n\n  // 1. Fetch market metadata from Gamma\n  market = FETCH gamma.GET('/markets/' + condition_id)\n  IF market IS NULL:\n    EMIT WARN 'STALE_DATA'\n    RETURN\n\n  // 2. Extract resolution source and rules\n  rules = market.resolution_rules\n  source = market.resolution_source\n  oracle_bond_pusd = 750  // UMA $750 pUSD bond\n\n  // 3. Validate rule completeness\n  IF rules IS NULL OR len(rules) == 0:\n    EMIT WARN 'RESOLUTIONRULEPARSER_MISSING_RULES'\n    RETURN\n\n  // 4. Check negRisk flag\n  neg_risk = market.get('negRisk', False)\n\n  // 5. Emit ObservationReport\n  EMIT ObservationReport {\n    report_id: gen_id(),\n    kind: 'ObservationReport',\n    condition_id: condition_id,\n    resolution_source: source,\n    resolution_rules_hash: hash(rules),\n    oracle_bond_pusd: oracle_bond_pusd,\n    neg_risk: neg_risk,\n    emitted_at_ms: now_ms()\n  }",
    "sdk_calls": [
      "gamma.GET('/markets/<condition_id>')",
      "data.GET('/markets/<condition_id>/resolution')",
      "internal.killswitch.status"
    ],
    "complexity": "O(1) per market; O(M) for bulk pre-load"
  },
  "wire_examples": {
    "input": {
      "label": "Gamma market metadata fetch for resolution rule parsing",
      "source": "gamma",
      "payload": {
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "question": "Will BTC close above $100k on Dec 31, 2026?",
        "resolution_source": "UMA Optimistic Oracle",
        "resolution_rules": "Resolves YES if Coinbase BTC/USD close price on Dec 31 2026 is >= 100000.",
        "negRisk": false,
        "timestamp_ms": 1746703000000
      }
    },
    "output": {
      "label": "ObservationReport \u2014 resolution rules parsed",
      "payload": {
        "report_id": "rep_rrp_0xf1a2_1746703000000",
        "trace_id": "trc_0xbeef0102030405060709",
        "bot_id": "intel.resolutionruleparser",
        "kind": "ObservationReport",
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "resolution_source": "UMA Optimistic Oracle",
        "resolution_rules_hash": "0xabcd1234",
        "oracle_bond_pusd": 750,
        "neg_risk": false,
        "emitted_at_ms": 1746703005000
      }
    }
  },
  "reason_codes": [
    {
      "code": "RESOLUTIONRULEPARSER_MISSING_RULES",
      "severity": "WARN",
      "meaning": "Market resolution_rules field is null or empty in Gamma API response.",
      "action": "Skip emission; log WARN for manual review.",
      "user_message": "Resolution rules for this market are not yet published."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "Gamma API is unresponsive or returned stale market metadata.",
      "action": "Suppress emission; alert on-call if condition persists > 5 min.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; all ResolutionRuleParser emissions suppressed.",
      "action": "Continue parsing but suppress ObservationReport emissions.",
      "user_message": "Rule parsing signals paused while trading is suspended system-wide."
    },
    {
      "code": "RESOLUTIONRULEPARSER_SOURCE_CHANGE",
      "severity": "WARN",
      "meaning": "Resolution source changed since last parse (e.g. from UMA to manual).",
      "action": "Emit ObservationReport with change_detected=true; flag for strategy review.",
      "user_message": "The resolution source for this market has changed."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_resolutionruleparser_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "resolution_source"
        ],
        "meaning": "ObservationReports emitted per resolution source type."
      },
      {
        "name": "polytraders_intel_resolutionruleparser_missing_rules_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Markets with missing or empty resolution rules skipped."
      },
      {
        "name": "polytraders_intel_resolutionruleparser_source_changes_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Markets where resolution source changed since last parse."
      }
    ],
    "alerts": [
      {
        "name": "ResolutionRuleParserMissingRulesHigh",
        "condition": "rate(polytraders_intel_resolutionruleparser_missing_rules_total[30m]) > 5",
        "severity": "warn",
        "runbook": "#runbook-resolutionruleparser-missing-rules"
      },
      {
        "name": "ResolutionRuleParserSourceChange",
        "condition": "rate(polytraders_intel_resolutionruleparser_source_changes_total[10m]) > 0",
        "severity": "warn",
        "runbook": "#runbook-resolutionruleparser-source-change"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / ResolutionRuleParser parse rate and source change events"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "postgres",
    "shape": "Per condition_id: last_rules_hash, last_resolution_source, last_parsed_at_ms.",
    "ttl": "Retained indefinitely for compliance; archived after market resolves",
    "recovery": "On cold start, reload last known state from Postgres; re-parse all active markets.",
    "size_estimate": "~1 KB per tracked market"
  },
  "concurrency": {
    "execution_model": "async bulk-poll at market open, then per-event on Gamma webhook",
    "max_in_flight": 10,
    "idempotency_key": "condition_id + rules_hash",
    "timeout_ms": 10000,
    "backpressure": "drop-after-buffer \u2014 queue max 100 pending parses",
    "locking": "Postgres row lock on condition_id during parse update"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Suppress emissions when KillSwitch is active."
      }
    ],
    "emits_to": [
      {
        "bot_id": "strat.resolution_aware_strategies",
        "what": "ObservationReport with resolution_source and rules hash for strategy risk adjustment"
      }
    ],
    "sibling": [
      "intel.sourceoftruthverifier"
    ],
    "external": [
      {
        "service": "Polymarket Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500 ms p99",
        "fallback": "Emit STALE_DATA if Gamma unreachable; retry on next poll"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Adversary modifies resolution rules in Gamma to manipulate downstream strategy behaviour",
      "Source change spoofing to trigger false RESOLUTIONRULEPARSER_SOURCE_CHANGE alerts"
    ],
    "mitigations": [
      "Rules hash comparison detects any rule text changes between parses",
      "ObservationReports are informational only \u2014 strategies independently verify before acting"
    ]
  },
  "failure_injection": [
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block Gamma API for 5 min",
      "expected_behaviour": "STALE_DATA WARN; no emissions; ResolutionRuleParserMissingRulesHigh alert may fire",
      "recovery": "Automatic on Gamma recovery; re-parse all active markets"
    },
    {
      "scenario": "RULES_CHANGE",
      "how_to_inject": "Modify resolution_rules for a live market in test Gamma instance",
      "expected_behaviour": "rules_hash mismatch detected; ObservationReport emitted with change_detected=true",
      "recovery": "Automatic; downstream strategies receive updated rules hash"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true during active parsing",
      "expected_behaviour": "All emissions suppressed; KILL_SWITCH_ACTIVE logged; parsing loop continues",
      "recovery": "Automatic on KillSwitch reset"
    }
  ],
  "runbook": {
    "summary": "ResolutionRuleParser incidents are typically Gamma API outages or unexpected rule changes. Rule changes require prompt strategy notification.",
    "oncall_actions": [
      {
        "alert": "ResolutionRuleParserMissingRulesHigh",
        "first_step": "Check Gamma API health. Verify markets have published resolution rules via Gamma dashboard.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if > 10 markets missing rules"
      },
      {
        "alert": "ResolutionRuleParserSourceChange",
        "first_step": "Review which condition_id triggered the change. Confirm with market data team if change is legitimate.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead immediately on any source change"
      }
    ],
    "manual_overrides": [
      {
        "command": "force_reparse_all",
        "effect": "POST /internal/resolutionruleparser/reparse to trigger bulk re-parse of all active markets \u2014 After Gamma API recovery from extended outage"
      }
    ],
    "healthcheck": "Endpoint: /internal/health/resolutionruleparser | Green: Last parse < 60 min ago AND Postgres reachable AND Gamma API returning 200 | Red: No parse for > 2 h OR Postgres unreachable OR Gamma API down > 15 min"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for missing-rules gate, source-change detection, and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Bulk parse of all active Gamma markets succeeds with 0 errors on staging",
        "how_measured": "Integration test",
        "threshold": "0 parse errors"
      }
    ],
    "to_general_live": [
      {
        "gate": "Rule-change detection verified on synthetic rule mutation over 7-day soak",
        "how_measured": "Integration test log audit",
        "threshold": "100% detection of injected changes"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "ObservationReport"
    ],
    "topics": [
      "polytraders.reports.observation"
    ],
    "cadence": "every-event",
    "retention_class": "1y",
    "retention_notes": "Retained 1 y for compliance audit",
    "sampling_rule": "emit-every",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "summary-only",
    "consumes_kinds": []
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 2,
    "phase_name": "Data normalisation",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}