{
  "schema_version": "1.0.0",
  "bot_id": "4.12",
  "bot_name": "RuleChangeMonitor",
  "slug": "rulechangemonitor",
  "layer": "Intelligence",
  "layer_key": "intel",
  "bot_class": "Signal Service",
  "authority": [
    "Read-only"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "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": "Detect and emit any post-listing edit to a market's resolution rule.",
  "why_it_matters": [
    {
      "failure": "Resolution rule edited under an open position",
      "consequence": "If a market's rule changes after a strategy entered, the basis for that trade may no longer exist. Without monitoring, the position is held through the change without anyone on the team being told.",
      "worked_example": {
        "setup": "Strategy entered 4,000 pUSD on YES at 0.58 in market 0x7e21 at 09:14 UTC, on the basis that source-of-truth = `apnews.com`. At 14:02 UTC the rule is silently edited to allow 'AP or Reuters'.",
        "without_bot": "The strategy holds the position through resolution. AP says NO, Reuters says YES, and the market settles on the first source it can confirm. The trade outcome no longer reflects the thesis.",
        "with_bot": "RuleChangeMonitor diffs the structured rule and emits `RULE_SEMANTIC_CHANGE` at 14:02:03. The strategy reads the event, flattens to 0 inventory at 0.61, and Governance opens a review ticket."
      }
    },
    {
      "failure": "Cosmetic vs semantic edits not distinguished",
      "consequence": "Many edits are punctuation or formatting; a few are substantive. Treating all edits the same either floods operators with false alarms or misses the one that matters."
    },
    {
      "failure": "Audit trail of rule history missing",
      "consequence": "Compliance and dispute reviews require a full history of every rule change with timestamps. Polymarket's UI does not expose this; the monitor builds the record the team needs."
    },
    {
      "failure": "Strategies can't auto-pause on rule change",
      "consequence": "A strategy that reads a rule-change event can decide to flatten or hold automatically. Without the event, the team is the only line of defence and must catch every edit by hand."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Active market resolution_rules and question text from Gamma API",
      "source": "gamma",
      "required": true,
      "use": "Fetch current rule text for comparison against stored snapshot hashes."
    }
  ],
  "internal_inputs": [
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Suppress all emissions when KillSwitch is active."
    },
    {
      "input": "Last known rule snapshots from Postgres",
      "source": "Postgres",
      "required": true,
      "use": "Baseline for hash comparison to detect rule changes."
    }
  ],
  "raw_params": [
    "poll_interval_s \u00b7 int",
    "severity_filter \u00b7 enum",
    "auto_pause_strategies \u00b7 list",
    "publish_to \u00b7 list"
  ],
  "parameters": [
    {
      "name": "poll_interval_s",
      "default": 300,
      "warning": 900,
      "hard": 3600,
      "controls": "Seconds between full market rule snapshot comparison cycles.",
      "why_default_matters": "300 s provides timely detection of rule changes on live markets without overloading Gamma.",
      "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 monitored regularly to catch any changes to active markets."
    },
    {
      "name": "max_markets_per_cycle",
      "default": 500,
      "warning": 800,
      "hard": 1000,
      "controls": "Maximum number of active markets to compare per poll cycle to bound Gamma API load.",
      "why_default_matters": "500 covers all active Polymarket markets without risking API rate limits.",
      "threshold_logic": [
        {
          "condition": "count <= 500",
          "action": "Normal"
        },
        {
          "condition": "500\u2013800",
          "action": "WARN \u2014 approaching API rate limit"
        },
        {
          "condition": "> 1000",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.max_markets_per_cycle > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Rule monitoring is designed to scale with the number of active markets."
    }
  ],
  "default_config": {
    "bot_id": "intel.rulechangemonitor",
    "version": "0.1.0",
    "mode": "planned",
    "defaults": {
      "poll_interval_s": 300,
      "max_markets_per_cycle": 500
    },
    "locked": {
      "poll_interval_s": {
        "max": 3600
      },
      "max_markets_per_cycle": {
        "max": 1000
      }
    }
  },
  "implementation_flow": [],
  "decision_logic": {
    "approve": "",
    "reshape_required": "",
    "reject": "",
    "warning_only": ""
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "report_id": "rep_rcm_0xf1a2_1746703000000",
    "trace_id": "trc_0xbeef0102030405060713",
    "bot_id": "intel.rulechangemonitor",
    "kind": "ObservationReport",
    "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
    "change_type": "resolution_rules",
    "old_hash": "0xabcd1234",
    "new_hash": "0xefgh5678",
    "change_detected": true,
    "emitted_at_ms": 1746703005000
  },
  "developer_log": {
    "bot_id": "intel.rulechangemonitor",
    "markets_checked": 312,
    "rule_changes_detected": 1,
    "question_changes_detected": 0,
    "gamma_response_ms": 850,
    "killswitch_active": false,
    "emitted_at_ms": 1746703005000
  },
  "user_explanations": [
    {
      "situation": "Strategy paused entry after rule change detected",
      "message": "The resolution conditions for this market were updated. The system flagged this change and paused new entries until the update was acknowledged."
    },
    {
      "situation": "No rule change detected during routine monitoring",
      "message": "All active market rules were verified to be unchanged in the latest monitoring cycle."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Gamma API outage prevents rule comparison for active markets, causing strategy layer to operate with potentially outdated resolution rule awareness during the outage window.",
    "false_positive_risk": "Minor non-substantive formatting changes (whitespace, punctuation) to rule text cause a rules_hash change and trigger a false rule-change alert.",
    "false_negative_risk": "Gamma API returns a cached stale response during a legitimate rule change, causing RuleChangeMonitor to miss the change until the cache expires.",
    "safe_fallback": "If Gamma API is unavailable for > poll_interval_s * 2, emit STALE_DATA WARN and suspend rule comparisons. Retain all last known snapshots in Postgres. Resume on next successful poll.",
    "required_dependencies": [
      "Polymarket Gamma API",
      "KillSwitch active flag",
      "Postgres for rule snapshot and audit log"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Rule change detected and ObservationReport emitted",
        "setup": "rules_hash changes between two consecutive polls for the same condition_id",
        "expected": "ObservationReport emitted with change_type=resolution_rules, old_hash, new_hash"
      },
      {
        "test": "No change: no ObservationReport emitted",
        "setup": "rules_hash identical between polls",
        "expected": "No ObservationReport; no alert"
      },
      {
        "test": "KillSwitch suppresses emission",
        "setup": "killswitch.active=true; rule change present",
        "expected": "No ObservationReport; KILL_SWITCH_ACTIVE logged"
      }
    ],
    "integration": [
      {
        "test": "Rule change detected, emitted, consumed by strategy layer for re-evaluation",
        "expected": "Strategy receives ObservationReport with change_detected=true and affected condition_id"
      },
      {
        "test": "Gamma API down: STALE_DATA emitted; snapshots retained in Postgres",
        "expected": "STALE_DATA WARN; all last-known snapshots preserved; monitoring resumes on recovery"
      }
    ],
    "property": [
      {
        "property": "RuleChangeMonitor never submits or signs orders",
        "required": "Always true"
      },
      {
        "property": "Postgres audit log receives an entry for every detected rule change",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Detect and emit any post-listing edit to a market's resolution rule.",
  "legacy_pm_signals": [
    "Rule-text diffs since listing",
    "Source-of-truth field changes",
    "Resolution-date adjustments"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "pretrade_intel"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "gamma",
    "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": false,
    "multichain_ready": false,
    "sdk_used": "py-clob-client-v2",
    "settlement_contract": "CTFExchangeV2",
    "notes": "Monitors Gamma API for resolution rule and question text changes across active markets. Read-only. No order signing."
  },
  "reference_implementation": {
    "pseudocode": "FUNCTION monitorRuleChanges():\n  // 0. KillSwitch check\n  IF FETCH internal.killswitch.status == ACTIVE:\n    RETURN\n\n  // 1. Fetch active markets from Gamma\n  markets = FETCH gamma.GET('/markets?status=active')\n  IF len(markets) == 0:\n    EMIT WARN 'STALE_DATA'\n    RETURN\n\n  // 2. Compare against stored snapshots\n  FOR m IN markets:\n    stored = db.get(m.condition_id)\n    IF stored IS NULL:\n      db.set(m.condition_id, snapshot(m))\n      CONTINUE\n\n    // 3. Detect rule or question changes\n    IF hash(m.resolution_rules) != stored.rules_hash:\n      EMIT WARN 'RULECHANGEMONITOR_RULE_CHANGED'\n      EMIT ObservationReport with change_type='resolution_rules'\n    IF hash(m.question) != stored.question_hash:\n      EMIT WARN 'RULECHANGEMONITOR_QUESTION_CHANGED'\n      EMIT ObservationReport with change_type='question'\n\n    // 4. Update snapshot\n    db.set(m.condition_id, snapshot(m))",
    "sdk_calls": [
      "gamma.GET('/markets?status=active')",
      "internal.killswitch.status"
    ],
    "complexity": "O(M) per poll cycle where M = active markets"
  },
  "wire_examples": {
    "input": {
      "label": "Gamma market response with changed resolution rules",
      "source": "gamma",
      "payload": {
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "resolution_rules": "Resolves YES if Coinbase BTC/USD close price on Dec 31 2026 is >= 100000. UPDATED: Binance price also accepted.",
        "timestamp_ms": 1746703000000
      }
    },
    "output": {
      "label": "ObservationReport \u2014 resolution rule changed",
      "payload": {
        "report_id": "rep_rcm_0xf1a2_1746703000000",
        "trace_id": "trc_0xbeef0102030405060713",
        "bot_id": "intel.rulechangemonitor",
        "kind": "ObservationReport",
        "condition_id": "0xf1a2b30000000000000000000000000000000000000000000000000000000000",
        "change_type": "resolution_rules",
        "old_hash": "0xabcd1234",
        "new_hash": "0xefgh5678",
        "emitted_at_ms": 1746703005000
      }
    }
  },
  "reason_codes": [
    {
      "code": "RULECHANGEMONITOR_RULE_CHANGED",
      "severity": "WARN",
      "meaning": "Resolution rules for an active market changed since last snapshot.",
      "action": "Emit ObservationReport with change_type=resolution_rules; notify strategy layer.",
      "user_message": "The resolution conditions for this market have changed."
    },
    {
      "code": "RULECHANGEMONITOR_QUESTION_CHANGED",
      "severity": "WARN",
      "meaning": "Question text for an active market changed since last snapshot.",
      "action": "Emit ObservationReport with change_type=question; flag for manual review.",
      "user_message": "The market question has been updated."
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "Gamma API returned empty or error response during rule monitoring poll.",
      "action": "Skip cycle; retain last snapshots; retry on next poll.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "KillSwitch active; all RuleChangeMonitor emissions suppressed.",
      "action": "Continue monitoring but suppress ObservationReport emissions.",
      "user_message": "Rule change signals paused while trading is suspended system-wide."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_intel_rulechangemonitor_observations_emitted_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "change_type"
        ],
        "meaning": "ObservationReports emitted per change type (resolution_rules, question)."
      },
      {
        "name": "polytraders_intel_rulechangemonitor_rule_changes_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Total resolution rule change events detected."
      },
      {
        "name": "polytraders_intel_rulechangemonitor_markets_monitored",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Number of active markets currently being monitored for rule changes."
      }
    ],
    "alerts": [
      {
        "name": "RuleChangeMonitorRuleChanged",
        "condition": "rate(polytraders_intel_rulechangemonitor_rule_changes_total[5m]) > 0",
        "severity": "warn",
        "runbook": "#runbook-rulechangemonitor-rule-changed"
      },
      {
        "name": "RuleChangeMonitorStale",
        "condition": "rate(polytraders_intel_rulechangemonitor_markets_monitored[30m]) == 0",
        "severity": "warn",
        "runbook": "#runbook-rulechangemonitor-stale"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Intelligence / RuleChangeMonitor rule-change event rate"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "postgres",
    "shape": "Per condition_id: rules_hash, question_hash, last_checked_at_ms. Compliance audit log of all detected changes.",
    "ttl": "Snapshots retained for 1 y for compliance; audit log retained indefinitely",
    "recovery": "On cold start, reload all snapshots from Postgres; run full comparison on first cycle.",
    "size_estimate": "~1 KB per tracked market; audit log grows at ~1 KB per change event"
  },
  "concurrency": {
    "execution_model": "single-threaded periodic batch poll",
    "max_in_flight": 1,
    "idempotency_key": "condition_id + new_rules_hash",
    "timeout_ms": 20000,
    "backpressure": "skip cycle if previous poll still in progress",
    "locking": "Postgres advisory lock on monitor_cycle key"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Suppress emissions when KillSwitch is active."
      }
    ],
    "emits_to": [
      {
        "bot_id": "strat.all_strategies",
        "what": "ObservationReport with change_type and condition_id for strategy risk re-evaluation"
      }
    ],
    "sibling": [
      "intel.resolutionruleparser"
    ],
    "external": [
      {
        "service": "Polymarket Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500 ms p99",
        "fallback": "Retain last snapshots; emit STALE_DATA if no successful poll for > 30 min"
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Adversary manipulates Gamma API response to inject false rule-change signals",
      "High-frequency rule updates flood RuleChangeMonitor with WARN events"
    ],
    "mitigations": [
      "Postgres audit log provides tamper-evident record of all rule changes",
      "Alert thresholds prevent single-event paging from minor text edits"
    ]
  },
  "failure_injection": [
    {
      "scenario": "GAMMA_API_DOWN",
      "how_to_inject": "Block Gamma API for 30 min",
      "expected_behaviour": "STALE_DATA WARN; no polls; RuleChangeMonitorStale alert fires after 30 min",
      "recovery": "Automatic on Gamma recovery; full comparison on first successful poll"
    },
    {
      "scenario": "RULE_CHANGE_INJECTION",
      "how_to_inject": "Modify resolution_rules in mock Gamma for a live condition_id",
      "expected_behaviour": "RULECHANGEMONITOR_RULE_CHANGED WARN; ObservationReport emitted; RuleChangeMonitorRuleChanged alert fires",
      "recovery": "Automatic once change acknowledged; snapshot updated"
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true during active monitoring",
      "expected_behaviour": "Polling continues; emissions suppressed; KILL_SWITCH_ACTIVE logged",
      "recovery": "Automatic on KillSwitch reset"
    }
  ],
  "runbook": {
    "summary": "RuleChangeMonitor alerts on resolution rule or question changes. Any rule change during a live position must be reviewed immediately. Postgres audit log is the compliance record.",
    "oncall_actions": [
      {
        "alert": "RuleChangeMonitorRuleChanged",
        "first_step": "Identify condition_id from alert payload. Verify rule change on Polymarket.com. Notify strategy team if positions are open on this market.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead immediately; market operations if change is unexpected"
      },
      {
        "alert": "RuleChangeMonitorStale",
        "first_step": "Check Gamma API health. Verify Postgres connectivity. Trigger manual resync if API is healthy.",
        "diagnosis": "",
        "mitigation": "",
        "escalation": "Intelligence pod lead if stale > 30 min"
      }
    ],
    "manual_overrides": [
      {
        "command": "force_resync",
        "effect": "POST /internal/rulechangemonitor/resync to force full snapshot comparison on all active markets \u2014 After Gamma API recovery or suspected missed changes"
      }
    ],
    "healthcheck": "Endpoint: /internal/health/rulechangemonitor | Green: Last poll < 30 min ago AND Postgres reachable AND Gamma API returning 200 | Red: No poll for > 1 h OR Postgres unreachable"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for rule change, question change, and KillSwitch suppression",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Full snapshot comparison of 500 active markets completes in < 20 s on staging",
        "how_measured": "Integration test",
        "threshold": "Completion time < 20 s"
      }
    ],
    "to_general_live": [
      {
        "gate": "100% detection of synthetic rule changes over 7-day soak",
        "how_measured": "Integration test log audit",
        "threshold": "100% detection"
      }
    ]
  },
  "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"
  }
}