{
  "openapi": "3.1.0",
  "info": {
    "title": "Heat Stress Work/Rest Guidance Calculator API",
    "version": "1.2.0",
    "summary": "Occupational heat-stress work/rest, hydration, and stop-work guidance across 93 standards.",
    "description": "Free public REST API that turns workplace heat conditions (WBGT, workload, acclimatization, shift window, PPE, location) into work/rest schedules, hydration guidance, and stop-work triggers. Evaluates 93 national, international, and industry standards and returns the most-protective composite (never an average). Decision support only — not a legal determination or a substitute for a qualified safety professional. Calls to /api/v1/* require a descriptive User-Agent (>= 8 chars, not a crawler/verified-bot UA) and are rate limited to 20 req/10s and 120 req/60s per IP (best-effort per datacenter; not flood protection). Agent guide: /skill.md. Discovery: /llms.txt.",
    "contact": {
      "name": "La Isla Network",
      "url": "https://heatrules.com"
    }
  },
  "servers": [
    {
      "url": "https://heatrules.com",
      "description": "Production (Cloudflare Pages)"
    },
    {
      "url": "http://localhost:3299",
      "description": "Local Express dev server"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness check",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/standards": {
      "get": {
        "operationId": "listStandards",
        "summary": "List all 93 registered standards with metadata",
        "responses": {
          "200": {
            "description": "Registry of standards.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "standards": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StandardMeta"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/compare": {
      "post": {
        "operationId": "compareStandards",
        "summary": "Evaluate all or selected standards and return the most-protective composite",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompareRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Comparison result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompareResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/standards/{standardId}/evaluate": {
      "post": {
        "operationId": "evaluateStandard",
        "summary": "Evaluate a single standard",
        "parameters": [
          {
            "name": "standardId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A standard id from /api/v1/standards (e.g. la_isla_network_rshs)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InputEnvelope"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Single-standard result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "inputSummary": {
                      "$ref": "#/components/schemas/InputSummary"
                    },
                    "standard": {
                      "$ref": "#/components/schemas/StandardResult"
                    },
                    "riskLevel": {
                      "type": "string"
                    },
                    "workRestSchedule": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "derivedOutputs": {
                      "$ref": "#/components/schemas/DerivedOutputs"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/guidance/country": {
      "post": {
        "operationId": "guidanceByCountry",
        "summary": "Compare standards filtered and ranked by country relevance",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z]{2}$"
            },
            "description": "ISO 3166-1 alpha-2 country code. Omit to get the unfiltered list."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompareRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Country-ranked comparison.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompareResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          }
        }
      }
    },
    "/api/v1/wbgt": {
      "get": {
        "operationId": "estimateWbgt",
        "summary": "Forecast-estimated WBGT for a coordinate",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          },
          {
            "name": "date",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "time",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
            }
          },
          {
            "name": "shiftStart",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
            }
          },
          {
            "name": "shiftEnd",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Estimated WBGT with confidence and method metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WbgtEstimate"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/weather/wbgt": {
      "get": {
        "operationId": "estimateWbgtInternal",
        "summary": "WBGT estimate (browser-app variant of /api/v1/wbgt)",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "date",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "shiftStart",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "shiftEnd",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Estimated WBGT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WbgtEstimate"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/weather/air-quality": {
      "get": {
        "operationId": "getAirQuality",
        "summary": "PM2.5-derived AQI for a coordinate (wildfire-smoke)",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Forecast horizon in days (1-7). When greater than 1, the response adds a `daily` array of per-day peak-PM2.5 AQI entries. Omit for the original single-day response shape.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 7,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PM2.5-specific AQI (EPA 2024 breakpoints), not a composite us_aqi.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "provider": {
                      "type": "string"
                    },
                    "latitude": {
                      "type": "number"
                    },
                    "longitude": {
                      "type": "number"
                    },
                    "aqi": {
                      "type": "object",
                      "properties": {
                        "value": {
                          "type": "number"
                        },
                        "pm25": {
                          "type": "number"
                        },
                        "pm10": {
                          "type": "number"
                        },
                        "sevKey": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string"
                        },
                        "guidance": {
                          "type": "string"
                        }
                      }
                    },
                    "daily": {
                      "type": "array",
                      "description": "Present only when `days` > 1. One entry per forecast day; `aqi` and `pm25` reflect the day's peak hourly PM2.5 (conservative screening), classified with EPA 2024 PM2.5 breakpoints.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "aqi": {
                            "type": "number"
                          },
                          "pm25": {
                            "type": "number"
                          },
                          "sevKey": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/geo/country": {
      "get": {
        "operationId": "getGeoCountry",
        "summary": "Cloudflare-detected country/region of the caller",
        "responses": {
          "200": {
            "description": "Detected geo (all null on local Express).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "country": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "regionCode": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "region": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/geo/search": {
      "get": {
        "operationId": "getGeoSearch",
        "summary": "Geocode a place name or US ZIP to candidate coordinates",
        "description": "Resolves a location for the manual fallback to browser geolocation. A `zip` (postal code, interpreted per `country` — e.g. US ZIP, Canadian FSA, UK outward code) is looked up via zippopotam.us; otherwise `q` (place name, 2+ chars) is searched via Open-Meteo Geocoding. The chosen result's lat/lon feed /api/v1/weather/wbgt.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Place name to search (2+ characters). Ignored when zip is present.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "zip",
            "in": "query",
            "required": false,
            "description": "Postal code, interpreted per country (default US). Takes precedence over q.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "ISO-3166 country code. For a zip lookup it selects the postal-code country (must be zippopotam-supported); for a place-name lookup it biases results.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching locations (possibly empty).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "admin1": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "countryCode": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "lat": {
                            "type": "number"
                          },
                          "lon": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Neither a valid q nor zip was provided.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/weather/es-heat-alert": {
      "get": {
        "operationId": "getEsHeatAlert",
        "summary": "Official AEMET heat aviso (naranja/rojo) for a Spanish worksite",
        "description": "Spain only. Reads the official AEMET avisos-geojson feed (keyless TopoJSON) and resolves the worksite point to the active ATTA (Temperaturas máximas) heat aviso, driving spain_rd_486_1997's aemetAlertLevel input (RDL 4/2023 §3). The aviso is zone-specific and forecaster-judged — it is never computed from temperature. Fail-loud contract: `status:\"none\"` is returned ONLY on a clean 200 with the point inside no ATTA zone; every failure (non-200, timeout, malformed body, decode failure, unknown level, Ceuta/Melilla source gap) returns `status:\"unavailable\"`, never a fabricated \"none\".",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          }
        ],
        "responses": {
          "200": {
            "description": "status ok (inside an ATTA aviso zone), none (clean check, no aviso), or unavailable (deterministic gap: Ceuta/Melilla or unknown level).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "none",
                        "unavailable"
                      ]
                    },
                    "level": {
                      "type": "string",
                      "enum": [
                        "amarillo",
                        "naranja",
                        "rojo"
                      ],
                      "description": "Present when status is ok."
                    },
                    "zoneCode": {
                      "type": "string"
                    },
                    "zoneName": {
                      "type": "string"
                    },
                    "valor": {
                      "type": "string"
                    },
                    "onset": {
                      "type": "string"
                    },
                    "expires": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string",
                      "description": "Present when status is unavailable (e.g. ceuta-melilla-source-gap, unknown-level)."
                    },
                    "source": {
                      "type": "string"
                    },
                    "attribution": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "status",
                    "source",
                    "attribution"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or non-numeric lat/lon.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "Upstream AEMET failure (non-200, timeout, malformed body, or decode failure). Body carries status:\"unavailable\" — never \"none\".",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "unavailable"
                      ]
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/weather/fr-vigilance": {
      "get": {
        "operationId": "getFrVigilance",
        "summary": "Official Météo-France canicule vigilance for a French worksite",
        "description": "France only. Resolves the worksite point to its department (keyless OpenDataSoft georef) and reads the official Météo-France canicule vigilance level (vert/jaune/orange/rouge), driving france_decret_2025_482's meteoFranceVigilance input (Décret 2025-482 / Arrêté du 27 mai 2025). The vigilance is department-specific and forecaster-judged — it is never computed from temperature. Primary is the keyless OpenDataSoft vigilance mirror; an optional keyed DPVigilance backup (METEOFRANCE_KEY) fires when the primary is unreachable. Fail-loud contract: `status:\"none\"` (vert) is returned ONLY on a clean check with the department resolved and no active canicule vigilance; every failure (unresolved department, non-200, timeout, malformed body, unrecognized colour) returns `status:\"unavailable\"`, never a fabricated \"none\".",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          }
        ],
        "responses": {
          "200": {
            "description": "status ok (active canicule vigilance jaune/orange/rouge), none (clean check, no active canicule vigilance), or unavailable (unresolved department or unrecognized colour).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "none",
                        "unavailable"
                      ]
                    },
                    "level": {
                      "type": "string",
                      "enum": [
                        "jaune",
                        "orange",
                        "rouge"
                      ],
                      "description": "Present when status is ok."
                    },
                    "department": {
                      "type": "string"
                    },
                    "departmentCode": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string",
                      "description": "Present when status is unavailable (e.g. departement-unresolved, unknown-colour)."
                    },
                    "source": {
                      "type": "string"
                    },
                    "attribution": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "status",
                    "source",
                    "attribution"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or non-numeric lat/lon.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "Upstream Météo-France failure (non-200, timeout, malformed body). Body carries status:\"unavailable\" — never \"none\".",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "unavailable"
                      ]
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/weather/at-heat-alert": {
      "get": {
        "operationId": "getAtHeatAlert",
        "summary": "Official GeoSphere Austria heat warning for an Austrian worksite",
        "description": "Austria only. Resolves the worksite point (WGS84 → EPSG:31287 Lambert projection) and reads the official GeoSphere Austria heat warning level (1–4) from the free, keyless warnings API (warnungen.zamg.at/wsapp/api/getWarnstatus), driving austria_hitzeschutz_verordnung's geoSphereWarningLevel input (Hitzeschutzverordnung 2026). The warning level is zone-specific and forecaster-judged — it is never computed from temperature. Fail-loud contract: `status:\"none\"` is returned ONLY on a clean 200 + valid FeatureCollection + the worksite point lying inside no (wtype=5) heat warning polygon. Every other outcome (non-200, timeout, malformed FeatureCollection, unrecognized level, missing wlevel on a heat feature, invalid polygon geometry) returns `status:\"unavailable\"`, never a fabricated \"none\".",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          }
        ],
        "responses": {
          "200": {
            "description": "status ok (active heat warning level 1–4), none (clean check, no active heat warning), or unavailable (unresolved department or unrecognized level).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "none",
                        "unavailable"
                      ]
                    },
                    "level": {
                      "type": "string",
                      "description": "Present when status is ok. One of 1 (gelb/Achtung), 2 (orange/Vorsicht), 3 (rot/Gefahr), 4 (extreme/Große Gefahr)."
                    },
                    "reason": {
                      "type": "string",
                      "description": "Present when status is unavailable (e.g. upstream-fetch-failed, invalid-topology, unknown-level)."
                    },
                    "rawLevel": {
                      "type": "string",
                      "description": "Present when status is unavailable and reason is unknown-level — the raw wlevel value from the upstream API."
                    },
                    "source": {
                      "type": "string"
                    },
                    "attribution": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "status",
                    "source",
                    "attribution"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or non-numeric lat/lon.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "ValidationError": {
        "description": "Input validation failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Request body exceeds the 64 KB limit. The bound protects the Workers isolate from response amplification; a full 7-day hourly series plus every registered standardId fits well inside it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Missing/short/crawler User-Agent, verified bot, or proxy-hosting ASN.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (20 req/10s burst, 120 req/60s sustained; best-effort per datacenter). See Retry-After header.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait before retrying."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "HeatInput": {
        "type": "object",
        "description": "Workplace heat conditions. Provide wbgtC (or wbgtF) and workload at minimum. Out-of-range values are rejected, never clamped. Empty strings are treated as absent.",
        "properties": {
          "wbgtC": {
            "type": "number",
            "minimum": -40,
            "maximum": 60,
            "description": "WBGT in Celsius."
          },
          "wbgtF": {
            "type": "number",
            "minimum": -40,
            "maximum": 140,
            "description": "WBGT in Fahrenheit (alternative to wbgtC)."
          },
          "wbgtUnit": {
            "type": "string",
            "enum": [
              "C",
              "F"
            ]
          },
          "wbgtInputMode": {
            "type": "string",
            "enum": [
              "single",
              "start_peak",
              "hourly"
            ]
          },
          "hourlyWbgt": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "time"
              ],
              "properties": {
                "time": {
                  "type": "string",
                  "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
                },
                "value": {
                  "type": "number"
                },
                "valueC": {
                  "type": "number",
                  "minimum": -40,
                  "maximum": 60
                },
                "valueF": {
                  "type": "number",
                  "minimum": -40,
                  "maximum": 140
                }
              }
            }
          },
          "workload": {
            "type": "string",
            "enum": [
              "light",
              "moderate",
              "heavy",
              "very_heavy"
            ]
          },
          "acclimatization": {
            "type": "string",
            "enum": [
              "unacclimatized",
              "partial",
              "acclimatized",
              "unknown",
              "new"
            ]
          },
          "shiftStart": {
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          },
          "shiftEnd": {
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "relativeHumidity": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "metabolicRateW": {
            "type": "number",
            "minimum": 1,
            "maximum": 1000
          },
          "sunExposure": {
            "type": "boolean"
          },
          "outdoor": {
            "type": "boolean"
          },
          "shade": {
            "type": "boolean"
          },
          "airTempC": {
            "type": "number",
            "minimum": -40,
            "maximum": 60
          },
          "wetBulbC": {
            "type": "number",
            "minimum": -40,
            "maximum": 60
          },
          "globeTempC": {
            "type": "number",
            "minimum": -40,
            "maximum": 100
          },
          "meanRadiantTempC": {
            "type": "number",
            "minimum": -40,
            "maximum": 100
          },
          "windSpeedMS": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "atmPressurehPa": {
            "type": "number",
            "minimum": 800,
            "maximum": 1100
          },
          "twlWm2": {
            "type": "number",
            "minimum": 0,
            "maximum": 600
          },
          "heatIndexF": {
            "type": "number",
            "minimum": -40,
            "maximum": 200
          },
          "maxTempC": {
            "type": "number",
            "minimum": -40,
            "maximum": 60
          },
          "indoorTempC": {
            "type": "number",
            "minimum": -40,
            "maximum": 60
          },
          "aqiPm25": {
            "type": "number",
            "minimum": 0,
            "maximum": 500
          },
          "pm25ugm3": {
            "type": "number",
            "minimum": 0,
            "maximum": 1000
          }
        }
      },
      "InputEnvelope": {
        "type": "object",
        "description": "Inputs may be sent flat or wrapped in an 'input' object.",
        "properties": {
          "input": {
            "$ref": "#/components/schemas/HeatInput"
          }
        }
      },
      "CompareRequest": {
        "type": "object",
        "properties": {
          "input": {
            "$ref": "#/components/schemas/HeatInput"
          },
          "standardIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional subset of standard ids. Omit to evaluate all 93."
          }
        }
      },
      "StandardMeta": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "uid": {
            "type": [
              "string",
              "null"
            ]
          },
          "displayName": {
            "type": "string"
          },
          "authority": {
            "type": "string"
          },
          "jurisdiction": {
            "type": [
              "string",
              "null"
            ]
          },
          "countryCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "regionCodes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "legalStatus": {
            "type": "string"
          },
          "ruleType": {
            "type": "string"
          },
          "priority": {
            "type": "number"
          },
          "requiredInputs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "optionalInputs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sourceIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "StandardResult": {
        "type": "object",
        "properties": {
          "standardId": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "applicable": {
            "type": "boolean"
          },
          "hiddenReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "missingInputs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "confidence": {
            "type": [
              "string",
              "null"
            ]
          },
          "effectiveWbgtC": {
            "type": [
              "number",
              "null"
            ]
          },
          "recommendation": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "explanation": {
            "type": [
              "string",
              "null"
            ]
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "legalStatus": {
            "type": "string"
          },
          "jurisdiction": {
            "type": [
              "string",
              "null"
            ]
          },
          "sourceIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "InputSummary": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "effectiveWbgtC": {
            "type": "number"
          },
          "workload": {
            "type": "string"
          },
          "acclimatization": {
            "type": "string"
          },
          "clothingAdjustmentC": {
            "type": "number"
          },
          "shiftStart": {
            "type": [
              "string",
              "null"
            ]
          },
          "shiftEnd": {
            "type": [
              "string",
              "null"
            ]
          },
          "date": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Composite": {
        "type": "object",
        "description": "Most-protective applicable schedule. Never an average.",
        "properties": {
          "stopWork": {
            "type": "boolean"
          },
          "workMinutesPerHour": {
            "type": [
              "number",
              "null"
            ]
          },
          "restMinutesPerHour": {
            "type": [
              "number",
              "null"
            ]
          },
          "note": {
            "type": "string"
          },
          "contributingStandards": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "advisoryStandards": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "DerivedOutputs": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "hydration": {
            "type": "object",
            "additionalProperties": true
          },
          "akiRisk": {
            "type": "object",
            "additionalProperties": true
          },
          "productivityImpact": {
            "type": "object",
            "additionalProperties": true
          },
          "calendarBlocks": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "scheduleSuggestion": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          }
        }
      },
      "CompareResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "countryCode": {
            "type": "string",
            "description": "Present on /api/v1/guidance/country."
          },
          "inputSummary": {
            "$ref": "#/components/schemas/InputSummary"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StandardResult"
            }
          },
          "composite": {
            "$ref": "#/components/schemas/Composite"
          },
          "derivedOutputs": {
            "$ref": "#/components/schemas/DerivedOutputs"
          }
        }
      },
      "WbgtEstimate": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "provider": {
            "type": "string",
            "description": "Which forecast provider produced the numbers: \"Open-Meteo\" (primary, global) or \"NWS\" (US fallback)."
          },
          "sourceUrl": {
            "type": "string",
            "description": "The exact provider request URL the numbers came from."
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "timezone": {
            "type": "string",
            "description": "IANA zone the hourly timestamps are expressed in (\"auto\" / \"local\" when the provider did not name one)."
          },
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When this response was computed (not when the forecast was issued)."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "forecast_estimate_physical_weather",
              "forecast_estimate_full_weather",
              "forecast_estimate_partial_weather"
            ],
            "description": "Estimate quality tier. physical = energy-balance globe solved on real solar geometry with no data-quality warnings; full = complete weather but assumed geometry or an approximated globe; partial = one or more warnings (the NWS path is always partial, since it supplies no shortwave radiation)."
          },
          "method": {
            "type": "string",
            "description": "Plain-language provenance sentence for the current-hour estimate: provider, which inputs were used, and which globe/wet-bulb models ran."
          },
          "weather": {
            "type": "object",
            "additionalProperties": true,
            "description": "Current-hour meteorological inputs: time, airTempC, relativeHumidity, dewPointC, windSpeedMS, solarRadiation, directRadiation, diffuseRadiation, directNormalIrradiance, surfacePressurehPa, isDay."
          },
          "lightningRisk": {
            "type": [
              "object",
              "null"
            ],
            "description": "Thunderstorm assessment over the shift window (or the nearest hour), or null when none is forecast. Advisory alongside WBGT — it never enters the WBGT estimate.",
            "properties": {
              "active": {
                "type": "boolean"
              },
              "level": {
                "type": "string",
                "enum": [
                  "low",
                  "moderate",
                  "high",
                  "severe"
                ]
              },
              "label": {
                "type": "string"
              },
              "summary": {
                "type": "string"
              },
              "guidance": {
                "type": "string",
                "description": "The universal 30/30 shelter rule: suspend outdoor work when thunder is heard or lightning is seen."
              },
              "source": {
                "type": "string",
                "description": "\"Open-Meteo WMO weather code\" or \"NWS hourly forecast\"."
              },
              "weatherCode": {
                "type": "number",
                "description": "WMO code 95/96/99 on the Open-Meteo path; absent on the NWS text path."
              },
              "time": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "wbgt": {
            "type": "object",
            "properties": {
              "wbgtC": {
                "type": "number"
              },
              "wetBulbC": {
                "type": "number"
              },
              "globeTempC": {
                "type": "number"
              },
              "globeMethod": {
                "type": "string",
                "description": "Globe model actually used: Liljegren 2008 energy balance (primary), Dimiceli/Piltz linear fallback, or screening solar/wind approximation. Also notes when solar geometry had to be assumed."
              },
              "wetBulbMethod": {
                "type": "string",
                "description": "Wet-bulb model actually used: Liljegren 2008 natural wet bulb, or the provider/Stull 2011 psychrometric fallback."
              },
              "uncertaintyC": {
                "type": "number",
                "description": "Per-hour model-form and wind-sensitivity band in degrees C, computed for this hour rather than a constant. Floored at the 0.05 C output rounding. Does NOT include the models residual error against a real WBGT instrument."
              },
              "uncertaintyTerms": {
                "type": "object",
                "description": "Individual contributions to uncertaintyC: globeModelC (spread between the two globe models), wetBulbModelC (cost of the psychrometric fallback, when used), windC (estimate re-run at +/-0.25 m/s).",
                "additionalProperties": {
                  "type": "number"
                }
              },
              "nearCalm": {
                "type": "boolean",
                "description": "True when a +/-0.25 m/s wind error would move this hour WBGT by 1 C or more. Warning only; enters no calculation."
              }
            }
          },
          "hourlyWbgt": {
            "type": "array",
            "description": "Per-hour WBGT estimates for the shift window plus context hours on either side, or every available forecast hour when no shift window was supplied.",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date",
                  "description": "Local calendar day of this hour."
                },
                "time": {
                  "type": "string",
                  "description": "Local wall-clock HH:MM."
                },
                "valueC": {
                  "type": "number",
                  "description": "Estimated WBGT for this hour, degrees C."
                },
                "tempC": {
                  "type": "number"
                },
                "relativeHumidity": {
                  "type": "number"
                },
                "windSpeedMS": {
                  "type": "number",
                  "description": "10 m wind speed, m/s."
                },
                "cloudCover": {
                  "type": "number"
                },
                "solarRadiation": {
                  "type": "number",
                  "description": "Shortwave radiation, W/m2. Modeled from sky cover on the NWS path."
                },
                "nearCalm": {
                  "type": "boolean",
                  "description": "Per-hour near-calm flag, same criterion as wbgt.nearCalm. Consumers picking a peak from this series need to know which hours are soft."
                },
                "uncertaintyC": {
                  "type": "number",
                  "description": "Per-hour model band in degrees C, same construction as wbgt.uncertaintyC."
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "forecast_estimate",
                    "forecast_estimate_nws_fallback"
                  ]
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
