{
  "schema_version": "1.0.0",
  "bot_id": "6.10",
  "bot_name": "ParameterChangeAuditor",
  "slug": "parameterchangeauditor",
  "layer": "Governance",
  "layer_key": "gov",
  "bot_class": "Governance Service",
  "authority": [
    "Explain"
  ],
  "status": "planned",
  "readiness": "Spec started",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Governance",
    "bot_class": "Governance Service",
    "authority": "Explain",
    "runs_before": "Any bot whose parameters change \u2014 audit hook fires pre-write",
    "runs_after": "Every parameter update across the Polytraders bot fleet",
    "applies_to": "Every bot parameter in the system",
    "default_mode": "shadow_only",
    "user_visible": "no",
    "developer_owner": "Polytraders core"
  },
  "purpose": "ParameterChangeAuditor intercepts every config edit across the Polytraders fleet, records what changed, who changed it, when, and the before/after values with replay-grade detail.",
  "why_it_matters": [
    {
      "failure": "Parameter change not recorded",
      "consequence": "Incidents cannot be root-caused; audit trails are incomplete for compliance."
    },
    {
      "failure": "P0 parameter changed without alert",
      "consequence": "A high-risk config change may go unnoticed until it causes a trading incident."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "None \u2014 pure internal config audit hook",
      "source": "internal",
      "required": false,
      "use": "N/A"
    }
  ],
  "internal_inputs": [
    {
      "input": "Pre-write config delta event from each bot's config store",
      "source": "internal",
      "required": true,
      "use": "Capture before/after parameter values and editor identity."
    },
    {
      "input": "Ticket reference from change management system",
      "source": "internal",
      "required": false,
      "use": "Link the change to an approved ticket if require_ticket_for applies."
    }
  ],
  "raw_params": [
    "retain_history_days \u00b7 int",
    "require_ticket_for \u00b7 list",
    "publish_to_audit_log \u00b7 bool",
    "alert_on_p0_change \u00b7 bool"
  ],
  "parameters": [
    {
      "name": "retain_history_days",
      "default": 365,
      "warning": 180,
      "hard": 365,
      "controls": "How many days of parameter change history to retain.",
      "why_default_matters": "365 days covers a full trading year for audit purposes.",
      "threshold_logic": [
        {
          "condition": "retain_history_days >= 365",
          "action": "Retain; normal operation"
        }
      ],
      "dev_check": "if p.retain_history_days < 90: emit('AUDIT_RETENTION_WARN')",
      "user_facing": "Config changes are kept on record for one year."
    },
    {
      "name": "alert_on_p0_change",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true, any change to a P0-tagged parameter triggers an immediate alert.",
      "why_default_matters": "P0 parameters are risk-critical; immediate alerting is mandatory.",
      "threshold_logic": [
        {
          "condition": "p0_change AND alert_on_p0_change=true",
          "action": "Emit P0_PARAMETER_CHANGED alert immediately"
        }
      ],
      "dev_check": "if param.tier == 'P0': alerting.emit('P0_PARAMETER_CHANGED')",
      "user_facing": ""
    }
  ],
  "default_config": {
    "bot_id": "gov.parameterchangeauditor",
    "version": "0.1.0",
    "mode": "shadow_only",
    "defaults": {
      "retain_history_days": 365,
      "require_ticket_for": [
        "P0",
        "P1"
      ],
      "publish_to_audit_log": true,
      "alert_on_p0_change": true
    }
  },
  "implementation_flow": [
    "Hook into the config store write path of every bot; intercept before-write events.",
    "Compute diff between old and new parameter values.",
    "Record: bot_slug, param_name, old_value, new_value, editor_id, environment, timestamp, ticket_ref.",
    "If param tier is in require_ticket_for and ticket_ref is absent, reject the change and emit AUDIT_TICKET_REQUIRED.",
    "If alert_on_p0_change and param tier is P0, emit P0_PARAMETER_CHANGED alert immediately.",
    "Emit OperationsReport(event_type=PARAM_CHANGED) to audit log.",
    "Enforce retain_history_days; purge records older than retention window."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 ParameterChangeAuditor does not approve changes; it records them.",
    "reshape_required": "Not applicable.",
    "reject": "Rejects changes to P0/P1 parameters missing a ticket reference.",
    "warning_only": "Emits AUDIT_RETENTION_WARN if retain_history_days < 90."
  },
  "decision_output_schema": "OperationsReport",
  "decision_output_example": {
    "report_id": "ops_paramchangeauditor_01HX9Z",
    "bot_id": "gov.parameterchangeauditor",
    "event_type": "PARAM_CHANGED",
    "audited_bot": "risk.liquidityguard",
    "param_name": "max_position_pusd",
    "old_value": 5000,
    "new_value": 7500,
    "editor_id": "alice@polytraders",
    "ticket_ref": "PTRD-1042",
    "param_tier": "P1",
    "changed_at": "2026-05-09T10:00:00Z",
    "report_kind": "OperationsReport",
    "topic": "polytraders.reports.operations"
  },
  "developer_log": {
    "bot_id": "gov.parameterchangeauditor",
    "event_type": "AUDIT_HOOK_FIRED",
    "audited_bot": "risk.liquidityguard",
    "param_name": "max_position_pusd",
    "diff": {
      "old": 5000,
      "new": 7500
    }
  },
  "user_explanations": [
    {
      "situation": "Parameter change recorded",
      "message": "A configuration change was recorded in the audit log with full before/after detail."
    },
    {
      "situation": "Change rejected \u2014 missing ticket",
      "message": "This parameter requires an approved change ticket before it can be modified."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Audit log store is unavailable; changes are written to the bot's config but not recorded in the audit trail.",
    "false_positive_risk": "A benign automated parameter update (e.g. TTL refresh) is flagged as a P0 change.",
    "false_negative_risk": "A config change bypasses the hook if applied directly to the underlying store.",
    "safe_fallback": "If audit log is unavailable, buffer the audit record in memory and flush on reconnect; never block the config write.",
    "required_dependencies": [
      "Internal audit log store (Postgres)",
      "Config store write hooks for all bots"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "P1 change without ticket reference is rejected",
        "setup": "param_tier=P1, ticket_ref=null",
        "expected": "AUDIT_TICKET_REQUIRED; change rejected"
      },
      {
        "test": "P0 change triggers immediate alert",
        "setup": "param_tier=P0, alert_on_p0_change=true",
        "expected": "P0_PARAMETER_CHANGED alert emitted"
      }
    ],
    "integration": [
      {
        "test": "End-to-end: config change \u2192 audit hook \u2192 OperationsReport on audit log",
        "expected": "OperationsReport with event_type=PARAM_CHANGED and full diff"
      }
    ],
    "property": [
      {
        "property": "Every config write fires the audit hook before the write completes",
        "required": "Always true \u2014 hook is synchronous pre-write"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Capture every config edit \u2014 what, who, when, before/after \u2014 with replay-grade detail.",
  "legacy_pm_signals": [
    "Diff of every parameter set per strategy and per guard",
    "Editor identity, environment, and ticket reference",
    "Auto-revert candidates flagged by drift or incident"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "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": "ParameterChangeAuditor is an internal config audit service; pUSD references appear only in logged parameter values for strategies."
  },
  "reference_implementation": {
    "pseudocode": "// ---- PRE-WRITE HOOK ----\nFUNCTION onConfigWrite(botSlug, paramName, oldVal, newVal, editorId, ticketRef):\n  tier = paramTierMap.get(botSlug + '.' + paramName, 'P2')\n  IF tier IN config.require_ticket_for AND ticketRef IS NULL:\n    EMIT OperationsReport(event_type='AUDIT_TICKET_REQUIRED',\n      audited_bot=botSlug, param_name=paramName)\n    RAISE ConfigError('AUDIT_TICKET_REQUIRED')\n  record = {\n    audited_bot: botSlug, param_name: paramName,\n    old_value: oldVal, new_value: newVal,\n    editor_id: editorId, ticket_ref: ticketRef,\n    param_tier: tier, changed_at: now()\n  }\n  IF auditLog.available():\n    postgres.insert('param_change_audit', record)\n  ELSE:\n    memBuffer.append(record)\n    alerting.emit('AUDIT_LOG_UNAVAILABLE')\n  IF tier == 'P0' AND config.alert_on_p0_change:\n    alerting.emit('P0_PARAMETER_CHANGED', record)\n  EMIT OperationsReport(event_type='PARAM_CHANGED', ...record)\n\n// ---- RETENTION PURGE (scheduled) ----\nFUNCTION purgeOldRecords():\n  cutoff = now() - days(config.retain_history_days)\n  postgres.deleteWhere('param_change_audit', changed_at < cutoff)",
    "sdk_calls": [
      "postgres.insert('param_change_audit', record)",
      "postgres.deleteWhere('param_change_audit', cutoff)",
      "alerting.emit('P0_PARAMETER_CHANGED', record)"
    ],
    "complexity": "O(1) per config write; O(N) for retention purge where N = records older than cutoff"
  },
  "wire_examples": {
    "input": {
      "label": "Config write event",
      "source": "internal.config_store",
      "payload": {
        "bot_slug": "risk.liquidityguard",
        "param_name": "max_position_pusd",
        "old_value": 5000,
        "new_value": 7500,
        "editor_id": "alice@polytraders",
        "ticket_ref": "PTRD-1042"
      }
    },
    "output": {
      "label": "OperationsReport \u2014 PARAM_CHANGED",
      "payload": {
        "report_id": "ops_paramchange_01HX9Z",
        "event_type": "PARAM_CHANGED",
        "audited_bot": "risk.liquidityguard",
        "param_name": "max_position_pusd",
        "old_value": 5000,
        "new_value": 7500,
        "report_kind": "OperationsReport"
      }
    }
  },
  "reason_codes": [
    {
      "code": "PARAM_CHANGED",
      "severity": "INFO",
      "meaning": "A parameter was successfully changed and recorded.",
      "action": "Log and emit OperationsReport.",
      "user_message": ""
    },
    {
      "code": "P0_PARAMETER_CHANGED",
      "severity": "WARN",
      "meaning": "A P0-tier parameter was changed; immediate alert required.",
      "action": "Emit alert to on-call.",
      "user_message": ""
    },
    {
      "code": "AUDIT_TICKET_REQUIRED",
      "severity": "HARD_REJECT",
      "meaning": "A P0/P1 parameter change was attempted without a ticket reference.",
      "action": "Reject change; emit alert.",
      "user_message": ""
    },
    {
      "code": "AUDIT_LOG_UNAVAILABLE",
      "severity": "WARN",
      "meaning": "Audit log store is unavailable; records buffered in memory.",
      "action": "Buffer and flush on reconnect.",
      "user_message": ""
    },
    {
      "code": "AUDIT_RETENTION_WARN",
      "severity": "WARN",
      "meaning": "retain_history_days < 90; below recommended minimum.",
      "action": "Emit WARN.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_gov_parameterchangeauditor_changes_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "bot_slug",
          "param_tier"
        ],
        "meaning": "Total parameter changes recorded."
      },
      {
        "name": "polytraders_gov_parameterchangeauditor_rejections_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "reason"
        ],
        "meaning": "Total changes rejected by audit hook."
      },
      {
        "name": "polytraders_gov_parameterchangeauditor_p0_alerts_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Total P0 parameter change alerts fired."
      },
      {
        "name": "polytraders_gov_parameterchangeauditor_buffer_size",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Records buffered in memory awaiting audit log flush."
      }
    ],
    "alerts": [
      {
        "name": "ParameterAuditorP0Change",
        "condition": "rate(polytraders_gov_parameterchangeauditor_p0_alerts_total[5m]) > 0",
        "severity": "P1",
        "runbook": "#runbook-parameterauditor-p0"
      },
      {
        "name": "ParameterAuditorLogUnavailable",
        "condition": "polytraders_gov_parameterchangeauditor_buffer_size > 100",
        "severity": "P1",
        "runbook": "#runbook-parameterauditor-log"
      }
    ]
  },
  "state": {
    "store": "postgres",
    "shape": "param_change_audit table: {id, audited_bot, param_name, old_value, new_value, editor_id, ticket_ref, param_tier, changed_at}",
    "ttl": "retain_history_days (default 365 days)",
    "recovery": "On restart, flush memory buffer to Postgres; no in-memory state required.",
    "size_estimate": "~1 KB per change record; ~10 MB for 10k changes per year"
  },
  "concurrency": {
    "execution_model": "synchronous pre-write hook; single-threaded per bot config store",
    "max_in_flight": 50,
    "idempotency_key": "audited_bot + param_name + changed_at",
    "timeout_ms": 500,
    "backpressure": "buffer-in-memory",
    "locking": "none (audit writes are append-only)"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "internal.config_store",
        "why": "Config store fires pre-write events to the audit hook.",
        "contract": "Every config write must trigger the hook."
      }
    ],
    "emits_to": [
      {
        "bot_id": "internal.governance_audit",
        "what": "OperationsReport on every parameter change"
      }
    ],
    "sibling": [
      {
        "bot_id": "gov.incidentcommander",
        "why": "IncidentCommander queries recent parameter changes during RCA.",
        "contract": "OperationsReport records are queryable by audited_bot and changed_at."
      }
    ],
    "external": [
      {
        "service": "Internal Postgres (audit log)",
        "endpoint": "postgres://internal",
        "sla": "99.9%",
        "failure_mode": "Buffer in memory; flush on reconnect; never block config writes."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Direct database write to bypass the audit hook",
      "Providing a fake ticket reference for a P0 change"
    ],
    "mitigations": [
      "Config store enforces hook invocation; direct DB writes are blocked by access controls",
      "Ticket references are validated against the change management system"
    ]
  },
  "failure_injection": [
    {
      "scenario": "AUDIT_LOG_UNAVAILABLE",
      "how_to_inject": "Kill Postgres connection during a parameter change",
      "expected_behaviour": "Change proceeds; record buffered in memory; AUDIT_LOG_UNAVAILABLE emitted",
      "recovery": "Buffer flushed when Postgres reconnects."
    },
    {
      "scenario": "P0_CHANGE_WITHOUT_TICKET",
      "how_to_inject": "Submit P0 parameter change with ticket_ref=null",
      "expected_behaviour": "AUDIT_TICKET_REQUIRED; change rejected",
      "recovery": "Resubmit with valid ticket reference."
    },
    {
      "scenario": "BUFFER_OVERFLOW",
      "how_to_inject": "Keep Postgres down for 10 minutes; submit 200 changes",
      "expected_behaviour": "AUDIT_LOG_UNAVAILABLE alert fires at buffer_size > 100",
      "recovery": "Restore Postgres; buffer flushes automatically."
    }
  ],
  "runbook": {
    "summary": "ParameterChangeAuditor incidents are usually audit log unavailability or P0 parameter alerts.",
    "oncall_actions": [
      {
        "alert": "ParameterAuditorP0Change",
        "first_action": "Review the change record; verify the editor and ticket reference.",
        "escalate_to": "Governance pod lead"
      },
      {
        "alert": "ParameterAuditorLogUnavailable",
        "first_action": "Check Postgres health; verify buffer is draining.",
        "escalate_to": "SRE on-call"
      }
    ],
    "manual_overrides": [
      {
        "name": "flush-buffer",
        "how": "polytraders gov auditor flush-buffer",
        "when": "Postgres is restored but buffer is not auto-flushing."
      }
    ],
    "healthcheck": "/internal/health/parameterchangeauditor \u2192 green if Postgres reachable; buffer_size == 0; last change recorded < 1h ago; red if buffer_size > 100 or Postgres unreachable"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Pre-write hook unit tests pass for all tier levels",
        "how_measured": "CI",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Audit log write integration test passes",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "P0 alert fires within 1s of P0 parameter change in staging",
        "how_measured": "Alert latency test",
        "threshold": "< 1s"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "OperationsReport"
    ],
    "topics": [
      "polytraders.reports.operations"
    ],
    "cadence": "every-period",
    "retention_class": "1y",
    "sampling_rule": "batched-1/min",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "no",
    "consumes_kinds": []
  },
  "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"
  }
}