{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "exchange_v3.json",
  "title": "CCXT Exchange Extraction Output",
  "description": "Per-exchange extraction output from ccxt_extract. Contains everything CCXT knows about a single exchange, organized into two layers: runtime values (QuickBEAM) and structural AST (OXC). Schema 3.0.0 (Task 117, breaking) replaces runtime.markets with the compact runtime.symbols_index derivation and drops structure.parse_methods + structure.ws_methods (extractors retained; discovery files still written for internal Phase 12 / Phase 15 consumers). See SCHEMA.md migration notes.",
  "type": "object",
  "required": ["schema_version", "extracted_at", "ccxt_version", "exchange", "runtime", "structure", "_provenance"],
  "additionalProperties": false,
  "properties": {
    "schema_version": {
      "const": "3.0.0",
      "description": "Schema version. Consumers should check this before parsing."
    },
    "extracted_at": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 timestamp of extraction."
    },
    "ccxt_version": {
      "type": "string",
      "description": "CCXT npm package version used for extraction."
    },
    "exchange": {
      "$ref": "#/$defs/ExchangeMeta"
    },
    "runtime": {
      "$ref": "#/$defs/RuntimeData"
    },
    "structure": {
      "$ref": "#/$defs/StructureData"
    },
    "_provenance": {
      "$ref": "#/$defs/ProvenanceMap",
      "description": "Required per-path provenance map (raw/derived/override). Required and non-null since schema 2.0.0 (Task 61c)."
    }
  },

  "$defs": {
    "ProvenanceMap": {
      "type": "object",
      "description": "Flat map keyed by RFC 6901 JSON Pointer strings into the payload. Values tag the source tier: raw (discovery passthrough), derived (assembly-time computation), override (priv/overrides/<id>.json).",
      "propertyNames": {
        "type": "string",
        "pattern": "^/"
      },
      "additionalProperties": {
        "type": "string",
        "enum": ["raw", "derived", "override"]
      }
    },
    "ExchangeMeta": {
      "type": "object",
      "description": "Exchange identity and metadata from CCXT runtime.",
      "required": ["id", "name", "alias"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "description": "CCXT exchange identifier (e.g., 'binance', 'kraken')."
        },
        "name": {
          "type": "string",
          "description": "Human-readable exchange name."
        },
        "certified": {
          "type": "boolean",
          "description": "Whether the exchange is CCXT-certified."
        },
        "pro": {
          "type": "boolean",
          "description": "Whether the exchange has WebSocket (pro) support."
        },
        "version": {
          "type": ["string", "null"],
          "description": "Exchange API version string."
        },
        "country": {
          "type": "array",
          "items": { "type": "string" },
          "description": "ISO country codes where the exchange is based."
        },
        "alias": {
          "type": "boolean",
          "description": "True if this is a re-brand alias (e.g., huobi -> htx)."
        },
        "referral": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/Referral" }
          ],
          "description": "Referral URL and discount, if any."
        },
        "tier": {
          "type": "string",
          "enum": ["tier1", "tier2", "tier3", "dex", "unclassified"],
          "description": "Priority tier for this project — hand-curated in priv/priority_tiers.json, not extracted from CCXT. tier1/tier2/dex are the buckets we commit to deriving recipes for; tier3 and unclassified still get full raw extraction."
        }
      }
    },

    "Referral": {
      "type": "object",
      "required": ["url"],
      "additionalProperties": false,
      "properties": {
        "url": { "type": "string" },
        "discount": { "type": ["number", "null"] }
      }
    },

    "RuntimeData": {
      "type": "object",
      "description": "Runtime values extracted via QuickBEAM JavaScript execution. Layer 1: what an exchange IS (configuration, capabilities, markets).",
      "required": ["describe", "symbols_index", "symbol_patterns", "url_templates", "testnet_urls"],
      "additionalProperties": false,
      "properties": {
        "describe": {
          "oneOf": [
            { "type": "null" },
            { "type": "object", "additionalProperties": true }
          ],
          "description": "Complete describe() output from CCXT runtime. Contains all configuration: api endpoints, has capabilities, fees, limits, urls, exceptions, features, timeframes, requiredCredentials, etc. May contain sentinel strings: '__undefined' (JS undefined values), '__function:<ClassName>' (JS function references with resolved CCXT error class names, e.g. '__function:ExchangeError'). Null for alias exchanges or when no usable describe data was assembled; check pipeline integrity stats to distinguish source gaps from expected nulls."
        },
        "symbols_index": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/SymbolsIndex" }
          ],
          "description": "Derived per-symbol spot/swap classification from loadMarkets(). Introduced at schema 3.0.0 (Task 117) as the compact replacement for the full runtime.markets snapshot. Consumers needing per-market price, precision, fees, limits, or exchange-native ids must call loadMarkets() at runtime — that was the only drift-safe path anyway. Null for alias exchanges, exchanges without cached market data, or when no usable markets data was assembled."
        },
        "symbol_patterns": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/SymbolPatterns" }
          ],
          "description": "Derived symbol formatting patterns per market type. Consumers use these rules for symbol conversion (unified <-> exchange-native). Null when no market data is available."
        },
        "url_templates": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/UrlTemplates" }
          ],
          "description": "URL templates per API section, showing base URL, sample path, and fully resolved URL from sign(). Consumers derive path prefixes injected by sign() (e.g., OKX's /api/v5/, Gate's /spot/). Null for alias exchanges or when sign() fails for all sections."
        },
        "testnet_urls": {
          "$ref": "#/$defs/TestnetUrls",
          "description": "Derived testnet / sandbox URL catalog with {hostname} placeholders pre-resolved. See $defs/TestnetUrls for pattern classification."
        }
      }
    },

    "TestnetUrls": {
      "type": "object",
      "description": "Testnet / sandbox URL catalog derived from describe.urls.test + describe.options.sandboxMode. Promotes the raw testnet signals out of the opaque runtime.describe blob so consumers read one canonical shape. Introduced at schema 2.4.0 (Task 100).",
      "required": ["pattern", "urls", "sandbox_flag_field", "unresolved_reason"],
      "additionalProperties": false,
      "properties": {
        "pattern": {
          "type": "string",
          "enum": ["separate_host", "sandbox_flag", "none"],
          "description": "Classification: 'separate_host' when describe.urls.test is non-empty; 'sandbox_flag' when only describe.options.sandboxMode is present; 'none' when neither is present."
        },
        "urls": {
          "oneOf": [
            { "type": "null" },
            { "type": "object", "additionalProperties": true }
          ],
          "description": "Testnet URL map from describe.urls.test with {hostname} substitutions resolved. Shape preserved from CCXT (flat section map OR nested host → section map). Non-null only when pattern == 'separate_host'."
        },
        "sandbox_flag_field": {
          "oneOf": [
            { "type": "null" },
            { "type": "string" }
          ],
          "description": "Name of the describe.options flag used to switch to sandbox mode (currently always 'sandboxMode' when present). Populated independently of pattern — may coexist with 'separate_host' (okx uses both a same-host URL AND a flag)."
        },
        "unresolved_reason": {
          "oneOf": [
            { "type": "null" },
            { "type": "string", "enum": ["no_testnet_data"] }
          ],
          "description": "Non-null only when pattern == 'none'. Explains why no testnet data was derived."
        }
      }
    },

    "SymbolsIndex": {
      "type": "object",
      "description": "Compact per-symbol spot/swap classification derived from loadMarkets(). Keys are CCXT unified symbols (e.g. 'BTC/USDT', 'BTC/USDT:USDT'). Introduced at schema 3.0.0 (Task 117) as the drift-safe replacement for the full runtime.markets snapshot. Consumers derive market_count via map size; for price/precision/fees/limits/info call loadMarkets() at runtime.",
      "additionalProperties": {
        "type": "object",
        "required": ["spot", "swap"],
        "additionalProperties": false,
        "properties": {
          "spot": {
            "type": "boolean",
            "description": "True when the source market carries spot == true or type == 'spot'."
          },
          "swap": {
            "type": "boolean",
            "description": "True when the source market carries swap == true or type == 'swap'."
          }
        }
      }
    },

    "SymbolPatterns": {
      "type": "object",
      "description": "Symbol formatting patterns derived from market data. Keys are market types ('spot', 'swap', 'future', 'option') mapping to per-type pattern analysis, plus 'currency_aliases' from describe().commonCurrencies.",
      "required": ["currency_aliases"],
      "properties": {
        "spot": { "$ref": "#/$defs/SymbolPatternEntry" },
        "swap": { "$ref": "#/$defs/SymbolPatternEntry" },
        "future": { "$ref": "#/$defs/SymbolPatternEntry" },
        "option": { "$ref": "#/$defs/SymbolPatternEntry" },
        "currency_aliases": {
          "type": "object",
          "additionalProperties": { "type": "string" },
          "description": "Exchange-native currency code -> CCXT unified code mapping from describe().commonCurrencies (e.g., 'XXBT' -> 'BTC')."
        }
      },
      "additionalProperties": false
    },

    "SymbolPatternEntry": {
      "type": "object",
      "description": "Symbol formatting pattern analysis for a single market type.",
      "required": ["id_structure", "separator", "case", "suffix", "sample_count", "anomaly_count", "anomalies", "examples"],
      "additionalProperties": false,
      "properties": {
        "id_structure": {
          "type": ["string", "null"],
          "description": "How the exchange ID is composed from currency codes: 'baseId_quoteId' (most common), 'quoteId_baseId', 'baseId_only', 'numeric', or 'opaque'. Null if no dominant pattern (below 80% threshold)."
        },
        "separator": {
          "type": ["string", "null"],
          "description": "Character between currency codes in the ID: '' (concatenated), '-' (dash), '_' (underscore). Null if no dominant pattern."
        },
        "case": {
          "type": ["string", "null"],
          "enum": ["upper", "lower", "mixed", null],
          "description": "Case convention of the exchange ID. Null if no dominant pattern."
        },
        "suffix": {
          "type": ["string", "null"],
          "description": "Constant suffix appended after the currency pair (e.g., '-SWAP', '-PERPETUAL', 'M'). Null if no suffix detected."
        },
        "sample_count": {
          "type": "integer",
          "description": "Number of markets analyzed for this type."
        },
        "anomaly_count": {
          "type": "integer",
          "description": "Number of markets that don't match the dominant pattern."
        },
        "anomalies": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Exchange IDs that don't match the dominant pattern (max 50). Consumers should use direct symbol->id lookup for these."
        },
        "examples": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["symbol", "id", "baseId", "quoteId"],
            "additionalProperties": false,
            "properties": {
              "symbol": { "type": "string" },
              "id": { "type": "string" },
              "baseId": { "type": "string" },
              "quoteId": { "type": "string" }
            }
          },
          "description": "Representative sample markets (max 3) showing the symbol-to-id mapping."
        }
      }
    },

    "UrlTemplates": {
      "type": "object",
      "description": "URL templates per API section. Keys are dot-joined section paths (e.g., 'public', 'private', 'public.spot', 'private.delivery'). Each entry records the raw sign() probe: inputs (api_param, http_method, sample_path) and output (resolved_url), plus a derived url_prefix when provably correct.",
      "additionalProperties": { "$ref": "#/$defs/UrlTemplateEntry" }
    },

    "UrlTemplateEntry": {
      "type": "object",
      "description": "Raw sign() probe for a single API section. Records what was passed to sign() and what it returned. url_prefix is derived (resolved_url minus trailing sample_path) only when provably correct — null for suffix-mutation exchanges (bit2c, lbank, zonda) and sign() failures.",
      "required": ["api_param", "http_method", "sample_path", "resolved_url", "url_prefix"],
      "additionalProperties": false,
      "properties": {
        "api_param": {
          "oneOf": [
            { "type": "string" },
            { "type": "array", "items": { "type": "string" } }
          ],
          "description": "Section identifier passed to sign(). String for flat sections ('public'), array for multi-level (['public', 'spot'])."
        },
        "http_method": {
          "type": "string",
          "description": "HTTP method of the sampled endpoint passed to sign() (GET, POST, PUT, DELETE, PATCH)."
        },
        "sample_path": {
          "type": "string",
          "description": "First endpoint path from this API section (used as sign() input)."
        },
        "resolved_url": {
          "type": ["string", "null"],
          "description": "Full URL returned by sign(). Null if sign() threw (e.g., private sections without credentials, or sections missing from urls.api)."
        },
        "url_prefix": {
          "type": ["string", "null"],
          "description": "Derived: resolved_url with sample_path stripped from the end. Null when resolved_url doesn't cleanly end with sample_path (suffix-mutation exchanges like bit2c, lbank, zonda) or when sign() failed. Consumers use this as the full URL prefix for the section."
        }
      }
    },

    "StructureData": {
      "type": "object",
      "description": "Structural data extracted via OXC TypeScript AST parsing. Layer 2: what an exchange DOES (code structure, method bodies).",
      "required": ["class_info", "methods", "sign_method", "authenticated_sections", "sign_recipe", "handle_errors", "interface_signatures", "pagination", "overrides", "unified_endpoints", "request_defaults"],
      "additionalProperties": false,
      "properties": {
        "class_info": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/ClassInfo" }
          ],
          "description": "Class hierarchy information. Null for alias exchanges without class entries or when no usable class info was assembled; check pipeline integrity stats to distinguish source gaps from expected nulls."
        },
        "methods": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/MethodInventory" }
          ],
          "description": "Method signature inventory (no AST bodies). Null for alias exchanges or when no usable method inventory was assembled; check pipeline integrity stats to distinguish source gaps from expected nulls."
        },
        "sign_method": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/MethodAST" }
          ],
          "description": "The sign() method AST for request authentication. Null if the exchange has no sign() method or is an alias."
        },
        "authenticated_sections": {
          "oneOf": [
            { "type": "null" },
            { "type": "array", "items": { "type": "string" } }
          ],
          "description": "API sections proven to require authentication via checkRequiredCredentials() gates in sign() AST. Handles api === 'X' and api[N] === 'X' patterns. Sorted. Null when sign() absent; empty list when sign() exists but no checkRequiredCredentials() gates found. Some exchanges authenticate without checkRequiredCredentials() — use sign_method AST for broader auth detection."
        },
        "sign_recipe": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/SignRecipeRecord" },
          "description": "Map of section name -> SignRecipeRecord. Keys mirror structure.authenticated_sections (contract-test invariant sign_recipe_keys_match_auth_sections enforces parity). Empty map when sign_method is null or no authenticated sections were detected. Scaffolded in schema 2.2.0; Task 64 emits all-null records with unresolved_reason = \"not_yet_derived\". Tasks 65–69 populate derivation fields incrementally. Standalone copy at priv/schema/sign_recipe_v1.json (must stay in sync)."
        },
        "handle_errors": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/HandleErrorsData" }
          ],
          "description": "The handleErrors() method AST plus exception mappings. Null if the exchange has no handleErrors(), is an alias, or no usable handleErrors data was assembled; check pipeline integrity stats to distinguish source gaps from expected nulls."
        },
        "interface_signatures": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": { "$ref": "#/$defs/InterfaceSignature" }
            }
          ],
          "description": "Map of API method name -> InterfaceSignature from abstract/*.ts interface declarations. These are the generated per-exchange typed endpoint methods (e.g., publicGetTicker, privatePostOrder). Null for alias exchanges, exchanges without abstract files, or when no usable data was assembled; check pipeline integrity stats to distinguish source gaps from expected nulls."
        },
        "pagination": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "properties": {
                "_unresolved": {
                  "type": "array",
                  "items": { "$ref": "#/$defs/PaginationEntry" },
                  "description": "Pagination calls where the target method name is a runtime variable (not a string literal). Each entry has target_method: null and containing_method set to the method body where the call was found."
                }
              },
              "additionalProperties": {
                "type": "array",
                "items": { "$ref": "#/$defs/PaginationEntry" }
              }
            }
          ],
          "description": "Map of target method name -> [PaginationEntry] showing which methods use pagination and what strategy. Values are always arrays (multiple entries when branch-dependent variants exist, e.g. coinbase fetchAccounts V2/V3). Optional _unresolved key holds entries with variable method names. Null for exchanges without pagination calls or when no usable pagination data was assembled."
        },
        "overrides": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/OverridesData" }
          ],
          "description": "Method override analysis for derived exchanges. Null for root exchanges (those extending Exchange directly) or when no usable override data was assembled; check pipeline integrity stats to distinguish source gaps from expected nulls."
        },
        "unified_endpoints": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": {
                "type": "array",
                "items": { "type": "string" }
              }
            }
          ],
          "description": "Map of unified method name -> list of interface method names called within that method body (e.g., 'fetchTicker' -> ['publicGetV5MarketTickers']). Multiple entries when the method branches by market type or API version. Alias/derived exchanges inherit parent mappings when they have no own unified endpoints. Null when no unified endpoint mappings were found (own or inherited)."
        },
        "request_defaults": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/RequestDefaults" }
          ],
          "description": "Map of method name -> per-key default request body entries extracted from literal ObjectExpressions that flow into this.<httpVerb>() calls. Per the Honesty Rule: resolvable primitives emit kind='literal' with value set; non-literal expressions emit kind='unresolved' with a closed-vocabulary reason tag (the value is null). Methods with no HTTP call, pure delegation, empty literal body, or divergent multiple-call-site bodies are silently absent. Null when no methods produced extractable defaults."
        }
      }
    },

    "RequestDefaults": {
      "type": "object",
      "description": "Map of method name -> (map of property key -> RequestDefaultsEntry). Keys are method names as they appear in the TypeScript class (e.g., 'fetchTime'). Per-property keys preserve the exchange's own string-literal keys verbatim; computed keys surface as the synthetic key '_computed'.",
      "additionalProperties": {
        "type": "object",
        "additionalProperties": { "$ref": "#/$defs/RequestDefaultsEntry" }
      }
    },

    "RequestDefaultsEntry": {
      "type": "object",
      "required": ["value", "kind", "reason"],
      "additionalProperties": false,
      "properties": {
        "value": {
          "description": "The resolved literal value (string, number, boolean, null, nested literal object, array of literals) when kind='literal'; always null when kind='unresolved'."
        },
        "kind": {
          "type": "string",
          "enum": ["literal", "unresolved"],
          "description": "'literal' means the value is a fully provable primitive or nested literal; 'unresolved' means the AST expression is non-literal and the value is null."
        },
        "reason": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "string",
              "enum": ["conditional_value", "identifier_reference", "dynamic_construction", "computed_key", "spread_elaboration"]
            }
          ],
          "description": "Null when kind='literal'. Otherwise a closed-vocabulary tag: 'conditional_value' (ternary / logical expression), 'identifier_reference' (variable or member access), 'dynamic_construction' (call, binary, template literal, partially-literal object/array), 'computed_key' (key was a computed expression), 'spread_elaboration' (reserved for future spread tracking)."
        }
      }
    },

    "SignRecipeRecord": {
      "type": "object",
      "additionalProperties": false,
      "required": ["crypto_op", "canonical_string", "signature_placement", "auth_headers", "nonce", "pre_sign_transforms", "unresolved_reason", "patch_count"],
      "description": "Declarative per-section signing recipe. Authoritative copy of priv/schema/sign_recipe_v1.json (kept in lockstep). See CcxtExtract.SignRecipe moduledoc and SCHEMA.md § Signing Recipe. All derivation fields are nullable: Task 64 ships every record with null derivation fields + unresolved_reason = \"not_yet_derived\". Tasks 65–69 populate fields incrementally.",
      "properties": {
        "crypto_op": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/SignRecipeCryptoOp" }],
          "description": "Hash / signature algorithm. Populated by Task 65."
        },
        "canonical_string": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "minProperties": 1,
              "additionalProperties": false,
              "patternProperties": {
                "^(GET|POST|PUT|DELETE|PATCH|\\*)$": { "$ref": "#/$defs/SignRecipeCanonicalString" }
              }
            }
          ],
          "description": "Per-verb map of canonical-string records. Keys are HTTP verb names (GET|POST|PUT|DELETE|PATCH) when sign() branches on method, or the sentinel `*` when every verb shares the same canonical string. A single section may mix families (e.g. GET = hmac_simple, POST = hmac_with_body). Populated by Tasks 66a (HMAC-simple entries) / 66b (HMAC-with-body entries)."
        },
        "signature_placement": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/SignRecipeSignaturePlacement" }],
          "description": "Where the final signature is attached to the request. Populated by Task 65."
        },
        "auth_headers": {
          "oneOf": [
            { "type": "null" },
            { "type": "array", "items": { "$ref": "#/$defs/SignRecipeAuthHeader" } }
          ],
          "description": "Ordered list of auth-related headers (excluding the signature header itself — see signature_placement). Populated by Task 67."
        },
        "nonce": {
          "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/SignRecipeNonce" }],
          "description": "Timestamp / nonce source and wire format. Populated by Task 67."
        },
        "pre_sign_transforms": {
          "oneOf": [
            { "type": "null" },
            { "type": "array", "items": { "$ref": "#/$defs/SignRecipePreSignTransform" } }
          ],
          "description": "Ordered transforms applied to the canonical string, body, or resulting signature before going on the wire. Populated by Task 68."
        },
        "unresolved_reason": {
          "oneOf": [
            { "type": "null" },
            { "type": "string", "enum": ["not_yet_derived", "custom_signing_family", "ambiguous_ast", "no_sign_method"] }
          ],
          "description": "Closed-vocabulary reason when derivation fields are null. \"not_yet_derived\" is the scaffold default. Must be null only when every derivation field above is non-null (enforced by Task 69)."
        },
        "patch_count": {
          "type": "integer",
          "minimum": 0,
          "description": "Three-Strikes counter (see CLAUDE.md). At 3, migrate the recipe to priv/overrides/<id>.json rather than continuing to stretch derivation."
        }
      }
    },

    "SignRecipeCryptoOp": {
      "type": "object",
      "additionalProperties": false,
      "required": ["algo"],
      "properties": {
        "algo": {
          "type": "string",
          "enum": ["hmac_sha256", "hmac_sha512", "hmac_sha384", "ed25519", "rsa", "custom"]
        },
        "reason": {
          "type": "string",
          "minLength": 1,
          "description": "Optional rationale; required only when algo = \"custom\"."
        }
      }
    },

    "SignRecipeCanonicalString": {
      "type": "object",
      "additionalProperties": false,
      "required": ["family", "components", "encoding"],
      "properties": {
        "family": {
          "type": "string",
          "enum": ["hmac_simple", "hmac_with_body", "jwt", "custom"]
        },
        "components": {
          "type": "array",
          "items": { "$ref": "#/$defs/SignRecipeCanonicalComponent" }
        },
        "encoding": {
          "type": "string",
          "enum": ["url_encoded", "json", "raw"]
        }
      }
    },

    "SignRecipeCanonicalComponent": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source"],
      "properties": {
        "source": {
          "type": "string",
          "enum": ["timestamp", "api_key", "recv_window", "method", "path", "query", "body", "literal"]
        },
        "value": {
          "type": "string",
          "description": "Required iff source = \"literal\"; absent otherwise."
        }
      }
    },

    "SignRecipeSignaturePlacement": {
      "type": "object",
      "additionalProperties": false,
      "required": ["location", "key"],
      "properties": {
        "location": {
          "type": "string",
          "enum": ["header", "query", "body"]
        },
        "key": {
          "type": "string",
          "minLength": 1
        }
      }
    },

    "SignRecipeAuthHeader": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "source"],
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "source": {
          "type": "string",
          "enum": ["api_key", "passphrase", "timestamp", "signature", "recv_window", "literal"]
        },
        "value": {
          "type": "string",
          "description": "Required iff source = \"literal\"; absent otherwise."
        }
      }
    },

    "SignRecipeNonce": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "format"],
      "properties": {
        "source": {
          "type": "string",
          "enum": ["timestamp_ms", "timestamp_sec", "timestamp_us", "timestamp_ns", "monotonic", "exchange_supplied"]
        },
        "format": {
          "type": "string",
          "enum": ["integer", "iso8601", "hex", "string"]
        }
      }
    },

    "SignRecipePreSignTransform": {
      "type": "object",
      "additionalProperties": false,
      "required": ["op", "target"],
      "properties": {
        "op": {
          "type": "string",
          "enum": ["hex_encode", "base64_encode", "lowercase", "url_encode", "json_encode"]
        },
        "target": {
          "type": "string",
          "enum": ["signature", "body", "canonical_string"]
        }
      }
    },

    "ClassInfo": {
      "type": "object",
      "required": ["rest"],
      "additionalProperties": false,
      "properties": {
        "rest": {
          "$ref": "#/$defs/ClassEntry",
          "description": "REST class information."
        },
        "ws": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/ClassEntry" }
          ],
          "description": "WS class information. Null if no WebSocket implementation."
        }
      }
    },

    "ClassEntry": {
      "type": "object",
      "required": ["node_key", "class_name", "extends_resolved", "parent_key", "file", "method_count"],
      "additionalProperties": false,
      "properties": {
        "node_key": {
          "type": "string",
          "description": "Unique identity: 'rest:<id>' or 'ws:<id>'."
        },
        "class_name": {
          "type": "string",
          "description": "TypeScript class name."
        },
        "id": {
          "type": "string",
          "description": "Exchange identifier (same as class_name for most exchanges)."
        },
        "type": {
          "type": "string",
          "enum": ["rest", "ws"],
          "description": "Class type: 'rest' for REST exchange, 'ws' for WebSocket exchange."
        },
        "extends_raw": {
          "type": "string",
          "description": "Raw extends value from AST (may be an import alias, e.g., 'binanceRest')."
        },
        "extends_resolved": {
          "type": "string",
          "description": "Resolved parent class name (e.g., 'binance' after alias resolution)."
        },
        "parent_key": {
          "type": "string",
          "description": "Parent node_key (e.g., 'Exchange', 'rest:binance')."
        },
        "file": {
          "type": "string",
          "description": "Source filename (e.g., 'binance.ts')."
        },
        "method_count": {
          "type": "integer",
          "description": "Number of methods defined in this class."
        },
        "methods": {
          "type": "array",
          "items": { "type": "string" },
          "description": "List of method names defined in this class."
        },
        "method_details": {
          "type": "array",
          "items": { "$ref": "#/$defs/MethodDetail" },
          "description": "Per-method metadata (name, async, param count, statement count)."
        }
      }
    },

    "MethodDetail": {
      "type": "object",
      "required": ["name", "async", "params", "statements"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string" },
        "async": { "type": "boolean" },
        "params": { "type": "integer", "description": "Parameter count." },
        "statements": { "type": "integer", "description": "Statement count in method body." }
      }
    },

    "MethodInventory": {
      "type": "object",
      "required": ["rest"],
      "additionalProperties": false,
      "properties": {
        "rest": {
          "type": "array",
          "items": { "$ref": "#/$defs/MethodSignature" },
          "description": "REST method signatures."
        },
        "ws": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "array",
              "items": { "$ref": "#/$defs/MethodSignature" }
            }
          ],
          "description": "WS method signatures. Null if no WebSocket implementation."
        }
      }
    },

    "MethodSignature": {
      "type": "object",
      "description": "Method signature without AST body.",
      "required": ["name", "async", "params", "return_type", "statements"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string" },
        "async": { "type": "boolean" },
        "params": {
          "type": "array",
          "items": { "$ref": "#/$defs/MethodParam" }
        },
        "return_type": {
          "type": ["string", "null"],
          "description": "TypeScript return type annotation, if present."
        },
        "statements": {
          "type": "integer",
          "description": "Statement count in method body."
        }
      }
    },

    "MethodParam": {
      "type": "object",
      "required": ["name"],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "description": "Parameter name."
        },
        "type": {
          "type": ["string", "null"],
          "description": "TypeScript type annotation, if present."
        }
      }
    },

    "InterfaceSignature": {
      "type": "object",
      "description": "Typed API method signature from abstract interface declaration. Has name, params, and return type but no method body (unlike MethodAST).",
      "required": ["name", "params", "return_type"],
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "description": "Method name (e.g., 'publicGetTicker', 'privatePostOrder')."
        },
        "params": {
          "type": "array",
          "items": { "$ref": "#/$defs/MethodParam" },
          "description": "Parameter list with names and TypeScript type annotations."
        },
        "return_type": {
          "type": ["string", "null"],
          "description": "TypeScript return type annotation (typically 'Promise<implicitReturnType>')."
        }
      }
    },

    "MethodAST": {
      "type": "object",
      "description": "Complete method with full ESTree AST body. Used for sign(), handleErrors(), parse*(), watch*(), handle*(), and override methods.",
      "required": ["async", "params", "return_type", "statements", "body"],
      "additionalProperties": false,
      "properties": {
        "async": { "type": "boolean" },
        "params": {
          "type": "array",
          "items": { "$ref": "#/$defs/MethodParam" }
        },
        "return_type": {
          "type": ["string", "null"]
        },
        "statements": {
          "type": "integer"
        },
        "body": {
          "$ref": "#/$defs/ASTNode",
          "description": "Complete ESTree BlockStatement AST. Preserves all node types, position info (start/end byte offsets), and nested structure."
        }
      }
    },

    "ASTNode": {
      "type": "object",
      "description": "ESTree AST node. Shape varies by node type. Always has 'type' field. Position info via 'start'/'end' byte offsets. Permissive schema — ESTree nodes are too varied to enumerate exhaustively.",
      "required": ["type"],
      "additionalProperties": true,
      "properties": {
        "type": {
          "type": "string",
          "description": "ESTree node type (e.g., 'BlockStatement', 'VariableDeclaration', 'CallExpression')."
        },
        "start": {
          "type": "integer",
          "description": "Start byte offset in source file."
        },
        "end": {
          "type": "integer",
          "description": "End byte offset in source file."
        }
      }
    },

    "HandleErrorsData": {
      "type": "object",
      "description": "handleErrors() method AST plus exception mappings from describe().",
      "required": ["method", "exceptions", "http_exceptions", "error_code_fields", "throw_dispatches"],
      "additionalProperties": false,
      "properties": {
        "method": {
          "$ref": "#/$defs/MethodAST",
          "description": "The handleErrors() method AST."
        },
        "exceptions": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": true,
              "description": "Exception mappings from describe().exceptions. Keys include 'broad' and 'exact' plus market-type-specific keys (e.g., 'spot', 'inverse', 'linear', 'option'). Values are objects mapping error strings to error class names."
            }
          ],
          "description": "Exception mappings from describe().exceptions. Null if not available."
        },
        "http_exceptions": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "additionalProperties": { "type": "string" },
              "description": "HTTP status code -> error class name mapping."
            }
          ],
          "description": "HTTP exception mappings from describe().httpExceptions. Null if not available."
        },
        "error_code_fields": {
          "type": "array",
          "description": "Derived from handleErrors() AST: all this.safeString/safeString2/safeValue calls that extract error codes and messages from API responses. Each entry records the object, field name, safe* method used, and alternate field (for safeString2).",
          "items": {
            "$ref": "#/$defs/ErrorCodeFieldEntry"
          }
        },
        "throw_dispatches": {
          "type": "array",
          "description": "Derived from handleErrors() AST: one entry per this.throwExactlyMatchedException / throwBroadlyMatchedException call. Each entry records which helper was called, the normalized exceptions-map source (exceptions/exceptions.exact/exceptions.broad/by_url.exact/by_url.broad/other), a raw string rendering of the source expression (anti-rot hatch), the resolved safe* binding for the lookup variable (arg[1]), and the unique resolved safe* binding referenced anywhere in the message expression (arg[2]) when one can be proven.",
          "items": {
            "$ref": "#/$defs/ThrowDispatchEntry"
          }
        }
      }
    },

    "ThrowDispatchEntry": {
      "type": "object",
      "description": "A single this.throw{Exactly,Broadly}MatchedException dispatch in handleErrors(). Pairs the exceptions-map source with the safe* binding that feeds the lookup argument, and with the unique safe* binding referenced by the message expression when derivable, so consumers can reconstruct code/message pairings without re-parsing the AST.",
      "required": ["helper", "exceptions_source", "exceptions_source_raw", "lookup", "message_lookup"],
      "additionalProperties": false,
      "properties": {
        "helper": {
          "type": "string",
          "enum": ["throwExactlyMatchedException", "throwBroadlyMatchedException"]
        },
        "exceptions_source": {
          "type": "string",
          "enum": ["exceptions", "exceptions.exact", "exceptions.broad", "by_url.exact", "by_url.broad", "other"],
          "description": "Normalized tag for arg[0]. 'exceptions' covers bare this.exceptions. 'exceptions.exact'/'exceptions.broad' cover both this.exceptions['exact'] and this.exceptions.exact shapes. 'by_url.*' covers this.getExceptionsByUrl(url, 'exact'|'broad'). 'other' is the anti-rot fallback — use exceptions_source_raw to disambiguate."
        },
        "exceptions_source_raw": {
          "type": "string",
          "description": "Compact string rendering of the arg[0] AST (always present). Anti-rot hatch so consumers can still inspect the original expression when exceptions_source is 'other'."
        },
        "lookup": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "required": ["object", "object_path", "field", "field2", "method"],
              "additionalProperties": false,
              "properties": {
                "object": { "oneOf": [{ "type": "null" }, { "type": "string" }] },
                "object_path": {
                  "oneOf": [
                    { "type": "null" },
                    { "type": "array", "items": { "type": "string" } }
                  ]
                },
                "field": { "oneOf": [{ "type": "null" }, { "type": "string" }, { "type": "integer" }] },
                "field2": { "oneOf": [{ "type": "null" }, { "type": "string" }, { "type": "integer" }] },
                "method": {
                  "oneOf": [
                    { "type": "null" },
                    { "type": "string", "enum": ["safeString", "safeString2", "safeValue"] }
                  ]
                }
              }
            }
          ],
          "description": "Resolved safe* binding for arg[1], or null when arg[1] is not a bound Identifier. Shape mirrors ErrorCodeFieldEntry minus roles/sentinel_values, plus method to save a cross-reference."
        },
        "message_lookup": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "object",
              "required": ["object", "object_path", "field", "field2", "method"],
              "additionalProperties": false,
              "properties": {
                "object": { "oneOf": [{ "type": "null" }, { "type": "string" }] },
                "object_path": {
                  "oneOf": [
                    { "type": "null" },
                    { "type": "array", "items": { "type": "string" } }
                  ]
                },
                "field": { "oneOf": [{ "type": "null" }, { "type": "string" }, { "type": "integer" }] },
                "field2": { "oneOf": [{ "type": "null" }, { "type": "string" }, { "type": "integer" }] },
                "method": {
                  "oneOf": [
                    { "type": "null" },
                    { "type": "string", "enum": ["safeString", "safeString2", "safeValue"] }
                  ]
                }
              }
            }
          ],
          "description": "Resolved safe* binding for the unique lookup value referenced anywhere in arg[2], or null when the message expression does not point at a single bound lookup value."
        }
      }
    },

    "ErrorCodeFieldEntry": {
      "type": "object",
      "description": "A single this.safe*(object, field) call found in handleErrors() AST, with role classification based on how the extracted value is used. Roles are derived structurally from the AST: throwExactlyMatchedException usage → error_code (value used as exact-map lookup key, typically a short identifier like Bybit's retCode or OKX's sCode — not necessarily numeric). throwBroadlyMatchedException usage → error_message (value is the message string scanned for substring matches in the broad-map). === / !== comparisons → status_sentinel with operator preserved for polarity detection. A field that flows through both throw helpers in the same handleErrors() accumulates both roles.",
      "required": ["object", "object_path", "field", "method", "field2", "roles", "sentinel_values"],
      "additionalProperties": false,
      "properties": {
        "object": {
          "oneOf": [{ "type": "null" }, { "type": "string" }],
          "description": "The identifier name of the first argument (e.g., 'response', 'error', 'data'). Null if the first argument is not an Identifier node."
        },
        "object_path": {
          "oneOf": [
            { "type": "null" },
            { "type": "array", "items": { "type": "string" } }
          ],
          "description": "Derivation path tracing the object back to the response parameter. E.g., ['response', 'data', 'failure', '0'] means the object was derived via response['data']['failure'][0]. Null when the object is 'response' directly (trivial path) or when the derivation chain cannot be resolved."
        },
        "field": {
          "oneOf": [{ "type": "null" }, { "type": "string" }, { "type": "integer" }],
          "description": "The literal value of the second argument — the field name being accessed (e.g., 'code', 'msg'). Integer for array index access. Null if not a Literal node."
        },
        "method": {
          "type": "string",
          "enum": ["safeString", "safeString2", "safeValue"],
          "description": "Which safe* method was called."
        },
        "field2": {
          "oneOf": [{ "type": "null" }, { "type": "string" }, { "type": "integer" }],
          "description": "For safeString2: the alternate field name (third argument). Null for safeString/safeValue or when not provided."
        },
        "roles": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": ["error_code", "error_message", "status_sentinel"]
          },
          "description": "How this field's value is used in handleErrors(). error_code: passed to throwExactlyMatchedException as the exact-map lookup key (dispatches to an exception class via `exceptions.exact[value]`). error_message: passed to throwBroadlyMatchedException as the string to scan for substrings of `exceptions.broad` keys. status_sentinel: compared against literal values via === or !== (see sentinel_values for captured literals and polarity). A field can have multiple roles when its value flows through more than one of these helpers. Empty array when the variable's usage is not structurally resolvable."
        },
        "sentinel_values": {
          "oneOf": [
            { "type": "null" },
            {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["value", "operator"],
                "additionalProperties": false,
                "properties": {
                  "value": { "type": "string", "description": "The literal value being compared against, stringified." },
                  "operator": { "type": "string", "enum": ["===", "!=="], "description": "The comparison operator used. Consumers can determine polarity: === with early-return means success value, !== with throw means success value (negated), === with throw means error value." }
                }
              }
            }
          ],
          "description": "Literal values compared against this field via === or !== operators, sorted by value. Each entry includes the operator for polarity detection. Null when the field has no status_sentinel role."
        }
      }
    },

    "PaginationEntry": {
      "type": "object",
      "description": "Pagination strategy and parameters for a single call site. Each entry includes provenance (containing_method, target_method). Strategy-specific fields (cursor_received, cursor_sent, cursor_increment, page_key) are present only for relevant strategies.",
      "required": ["strategy", "containing_method", "target_method"],
      "additionalProperties": true,
      "properties": {
        "strategy": {
          "type": "string",
          "enum": ["dynamic", "deterministic", "cursor", "incremental"],
          "description": "Pagination strategy type."
        },
        "containing_method": {
          "type": "string",
          "description": "Name of the method body where this pagination call was found (e.g. 'fetchAccountsV2')."
        },
        "target_method": {
          "type": ["string", "null"],
          "description": "Target method name passed to fetchPaginatedCall* (first argument). Null when the argument is a runtime variable rather than a string literal."
        },
        "max_entries_per_request": {
          "type": ["integer", "null"],
          "description": "Maximum entries per paginated API call. Null when not statically resolvable from source."
        },
        "cursor_received": {
          "type": ["string", "null"],
          "description": "Field name in response to extract next cursor (cursor strategy only)."
        },
        "cursor_sent": {
          "type": ["string", "null"],
          "description": "Field name to send cursor in request (cursor strategy only)."
        },
        "cursor_increment": {
          "type": ["integer", "null"],
          "description": "Numeric increment for cursor values (cursor strategy only)."
        },
        "page_key": {
          "type": ["string", "null"],
          "description": "Parameter name for page number (incremental strategy only)."
        }
      }
    },

    "OverridesData": {
      "type": "object",
      "description": "Method override analysis for exchanges that extend another exchange. Grouped by REST/WS class.",
      "required": ["extends", "rest", "ws"],
      "additionalProperties": false,
      "properties": {
        "extends": {
          "type": "string",
          "description": "Parent exchange id (e.g., 'binance' for binanceus)."
        },
        "rest": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/OverrideEntry" }
          ],
          "description": "REST class override details. Null if no REST-specific override."
        },
        "ws": {
          "oneOf": [
            { "type": "null" },
            { "$ref": "#/$defs/OverrideEntry" }
          ],
          "description": "WS class override details. Null if no WS-specific override."
        }
      }
    },

    "OverrideEntry": {
      "type": "object",
      "description": "Override details for a single class (REST or WS).",
      "required": ["parent_key", "overridden", "new_methods", "inherited"],
      "additionalProperties": false,
      "properties": {
        "parent_key": {
          "type": "string",
          "description": "Parent node_key (e.g., 'rest:binance')."
        },
        "overridden": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/MethodAST" },
          "description": "Map of overridden method name -> MethodAST. These methods exist on the parent and are redefined."
        },
        "new_methods": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/MethodAST" },
          "description": "Map of new method name -> MethodAST. These methods don't exist on the parent."
        },
        "inherited": {
          "type": "array",
          "items": { "type": "string" },
          "description": "List of method names inherited from the parent (names only, no AST)."
        }
      }
    }
  }
}
