{
  "openapi": "3.1.1",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "GraftonHub Public API",
    "version": "1.0.0",
    "description": "Read-only, source-aware local routing and live context for Grafton and nearby southern Lorain County communities. GraftonHub is not an official authority. Follow official/provider sources before acting on critical, time-sensitive, or address-specific details.",
    "contact": {
      "name": "GraftonHub corrections",
      "url": "https://graftonhub.com/feedback/?type=correction"
    },
    "license": {
      "name": "Public website terms and source-attribution rules",
      "url": "https://graftonhub.com/about/"
    }
  },
  "servers": [
    {
      "url": "https://graftonhub.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Search",
      "description": "Deterministic search over reviewed answers and resident guides."
    },
    {
      "name": "Live context",
      "description": "Current weather and hazard snapshots with explicit coverage status."
    },
    {
      "name": "Community data",
      "description": "Versioned source, entity, answer, freshness, and routing datasets."
    },
    {
      "name": "Discovery",
      "description": "Standards-based public API discovery."
    }
  ],
  "paths": {
    "/api/search": {
      "get": {
        "operationId": "searchGraftonHub",
        "summary": "Search reviewed local answers and guides",
        "description": "Returns deterministic ranked results. Community is never inferred from the query. Postal codes are rejected because they do not prove municipal or township identity. Each result says whether community context was actually applied; direct sources are suppressed when it was not.",
        "tags": ["Search"],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Resident question or compact search phrase.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 160
            },
            "example": "trash pickup"
          },
          {
            "name": "community",
            "in": "query",
            "required": false,
            "description": "Canonical community ID, exact name, or exact non-postal alias. Postal codes return HTTP 409 because they do not prove municipal or township identity. Use canonical IDs for durable integrations: grafton-village, grafton-township, lagrange-village, lagrange-township, carlisle-township, eaton-township, or oberlin.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "eaton-township"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum ranked results.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 3
            }
          }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/SearchSuccess" },
          "400": { "$ref": "#/components/responses/SearchError" },
          "409": { "$ref": "#/components/responses/SearchError" },
          "500": { "$ref": "#/components/responses/SearchError" }
        }
      },
      "head": {
        "operationId": "headSearchGraftonHub",
        "summary": "Inspect search status and response headers",
        "description": "Performs the same parameter validation as GET and returns no body.",
        "tags": ["Search"],
        "parameters": [
          { "$ref": "#/components/parameters/SearchQuery" },
          { "$ref": "#/components/parameters/Community" },
          { "$ref": "#/components/parameters/Limit" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/HeadSuccess" },
          "400": { "$ref": "#/components/responses/HeadError" },
          "409": { "$ref": "#/components/responses/HeadError" },
          "500": { "$ref": "#/components/responses/HeadError" }
        }
      },
      "options": {
        "operationId": "optionsSearchGraftonHub",
        "summary": "Inspect supported read-only search methods",
        "tags": ["Search"],
        "responses": {
          "204": { "$ref": "#/components/responses/OptionsSuccess" }
        }
      }
    },
    "/api/weather/current/": {
      "get": {
        "operationId": "getCurrentWeather",
        "summary": "Get the current weather snapshot",
        "description": "Inspect status.state, status.checkedAt, status.dataTimestamp, and status.complete. A degraded or incomplete response is not proof of current conditions.",
        "tags": ["Live context"],
        "responses": {
          "200": { "$ref": "#/components/responses/WeatherSuccess" },
          "503": { "$ref": "#/components/responses/SnapshotUnavailable" }
        }
      },
      "head": {
        "operationId": "headCurrentWeather",
        "summary": "Inspect current weather response headers",
        "tags": ["Live context"],
        "responses": {
          "200": { "$ref": "#/components/responses/HeadSuccess" },
          "503": { "$ref": "#/components/responses/HeadError" }
        }
      },
      "options": {
        "operationId": "optionsCurrentWeather",
        "summary": "Inspect supported weather endpoint methods",
        "tags": ["Live context"],
        "responses": {
          "204": { "$ref": "#/components/responses/OptionsSuccess" }
        }
      }
    },
    "/api/hazards/current/": {
      "get": {
        "operationId": "getCurrentHazards",
        "summary": "Get the current hazard snapshot",
        "description": "An empty items array is an all-clear only when status.complete and liveStatus.isComplete are both true. Official NWS, SPC, and emergency sources control action.",
        "tags": ["Live context"],
        "responses": {
          "200": { "$ref": "#/components/responses/HazardsSuccess" },
          "404": { "$ref": "#/components/responses/FixtureUnavailable" },
          "503": { "$ref": "#/components/responses/SnapshotUnavailable" }
        }
      },
      "head": {
        "operationId": "headCurrentHazards",
        "summary": "Inspect current hazards response headers",
        "tags": ["Live context"],
        "responses": {
          "200": { "$ref": "#/components/responses/HeadSuccess" },
          "404": { "$ref": "#/components/responses/HeadError" },
          "503": { "$ref": "#/components/responses/HeadError" }
        }
      },
      "options": {
        "operationId": "optionsCurrentHazards",
        "summary": "Inspect supported hazards endpoint methods",
        "tags": ["Live context"],
        "responses": {
          "204": { "$ref": "#/components/responses/OptionsSuccess" }
        }
      }
    },
    "/.well-known/api-catalog": {
      "get": {
        "operationId": "getApiCatalog",
        "summary": "Get the RFC 9727 API catalog",
        "tags": ["Discovery"],
        "responses": {
          "200": { "$ref": "#/components/responses/ApiCatalog" }
        }
      },
      "head": {
        "operationId": "headApiCatalog",
        "summary": "Discover the API catalog using HTTP headers",
        "tags": ["Discovery"],
        "responses": {
          "200": { "$ref": "#/components/responses/HeadSuccess" }
        }
      },
      "options": {
        "operationId": "optionsApiCatalog",
        "summary": "Inspect supported catalog methods",
        "tags": ["Discovery"],
        "responses": {
          "204": { "$ref": "#/components/responses/OptionsSuccess" }
        }
      }
    },
    "/data/community-index.json": {
      "get": {
        "operationId": "getCommunityIndex",
        "summary": "Get guide-page routing and trust records",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/CommunityIndex" } }
      },
      "head": {
        "operationId": "headCommunityIndex",
        "summary": "Inspect the community index resource",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/StaticHeadSuccess" } }
      }
    },
    "/data/community-source-registry.json": {
      "get": {
        "operationId": "getSourceRegistry",
        "summary": "Get the curated source registry",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/SourceRegistry" } }
      },
      "head": {
        "operationId": "headSourceRegistry",
        "summary": "Inspect the source registry resource",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/StaticHeadSuccess" } }
      }
    },
    "/data/community-entities.json": {
      "get": {
        "operationId": "getCommunityEntities",
        "summary": "Get official/provider-backed community entities",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/CommunityEntities" } }
      },
      "head": {
        "operationId": "headCommunityEntities",
        "summary": "Inspect the community entities resource",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/StaticHeadSuccess" } }
      }
    },
    "/data/local-answers.json": {
      "get": {
        "operationId": "getLocalAnswers",
        "summary": "Get source-aware local routing answers",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/LocalAnswers" } }
      },
      "head": {
        "operationId": "headLocalAnswers",
        "summary": "Inspect the local answers resource",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/StaticHeadSuccess" } }
      }
    },
    "/data/freshness.json": {
      "get": {
        "operationId": "getFreshnessAudit",
        "summary": "Get the public data freshness audit",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/FreshnessAudit" } }
      },
      "head": {
        "operationId": "headFreshnessAudit",
        "summary": "Inspect the freshness audit resource",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/StaticHeadSuccess" } }
      }
    },
    "/data/editor-queue.json": {
      "get": {
        "operationId": "getEditorQueue",
        "summary": "Get maintenance tasks that require review",
        "description": "Queue items are maintenance tasks, not authoritative resident answers.",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/EditorQueue" } }
      },
      "head": {
        "operationId": "headEditorQueue",
        "summary": "Inspect the editor queue resource",
        "tags": ["Community data"],
        "responses": { "200": { "$ref": "#/components/responses/StaticHeadSuccess" } }
      }
    }
  },
  "components": {
    "parameters": {
      "SearchQuery": {
        "name": "q",
        "in": "query",
        "required": true,
        "description": "Resident question or compact search phrase.",
        "schema": { "type": "string", "minLength": 1, "maxLength": 160 }
      },
      "Community": {
        "name": "community",
        "in": "query",
        "required": false,
        "description": "Canonical community ID, exact name, or exact non-postal alias. Postal codes intentionally return HTTP 409.",
        "schema": { "type": "string", "minLength": 1 }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Maximum ranked results.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 3 }
      }
    },
    "headers": {
      "Allow": {
        "description": "Supported read-only methods.",
        "schema": { "type": "string", "const": "GET, HEAD, OPTIONS" }
      },
      "CorsOrigin": {
        "description": "Public read-only cross-origin access.",
        "schema": { "type": "string", "const": "*" }
      },
      "DiscoveryLink": {
        "description": "Links to the API catalog, OpenAPI description, and human documentation.",
        "schema": { "type": "string" }
      },
      "DataStatus": {
        "description": "Current snapshot coverage state.",
        "schema": { "enum": ["current", "degraded", "unavailable"] }
      }
    },
    "responses": {
      "SearchSuccess": {
        "description": "Ranked results, or an empty result set with a no_close_match warning.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "./schemas/v1/search-response.schema.json" }
          }
        }
      },
      "SearchError": {
        "description": "A stable v1 validation, ambiguity, or internal error.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "./schemas/v1/error-response.schema.json" }
          }
        }
      },
      "WeatherSuccess": {
        "description": "Current or visibly degraded weather snapshot.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" },
          "X-GraftonHub-Data-Status": { "$ref": "#/components/headers/DataStatus" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "./schemas/v1/weather-snapshot.schema.json" }
          }
        }
      },
      "HazardsSuccess": {
        "description": "Current or visibly degraded hazard snapshot.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" },
          "X-GraftonHub-Data-Status": { "$ref": "#/components/headers/DataStatus" }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "./schemas/v1/hazards-snapshot.schema.json" }
          }
        }
      },
      "SnapshotUnavailable": {
        "description": "No snapshot passed the freshness and structure gates.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" },
          "X-GraftonHub-Data-Status": { "$ref": "#/components/headers/DataStatus" }
        },
        "content": {
          "application/json": { "schema": { "type": "object" } }
        }
      },
      "FixtureUnavailable": {
        "description": "Hazard test fixtures are intentionally unavailable on the production host.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["error"],
              "properties": { "error": { "type": "string", "minLength": 1 } },
              "additionalProperties": false
            }
          }
        }
      },
      "ApiCatalog": {
        "description": "RFC 9727 Linkset JSON API catalog.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        },
        "content": {
          "application/linkset+json": {
            "schema": {
              "type": "object",
              "required": ["linkset"],
              "properties": {
                "linkset": { "type": "array", "items": { "type": "object" } }
              },
              "additionalProperties": false
            }
          }
        }
      },
      "HeadSuccess": {
        "description": "The corresponding GET operation is available; no response body.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        }
      },
      "StaticHeadSuccess": {
        "description": "The static JSON resource is available; no response body.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        }
      },
      "HeadError": {
        "description": "The corresponding GET request would fail validation or availability checks; no response body.",
        "headers": {
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        }
      },
      "OptionsSuccess": {
        "description": "Read-only method and CORS metadata.",
        "headers": {
          "Allow": { "$ref": "#/components/headers/Allow" },
          "Access-Control-Allow-Origin": { "$ref": "#/components/headers/CorsOrigin" },
          "Link": { "$ref": "#/components/headers/DiscoveryLink" }
        }
      },
      "CommunityIndex": {
        "description": "Guide-page routing and trust records.",
        "content": { "application/json": { "schema": { "$ref": "./schemas/v1/community-index.schema.json" } } }
      },
      "SourceRegistry": {
        "description": "Curated source registry.",
        "content": { "application/json": { "schema": { "$ref": "./schemas/v1/source-registry.schema.json" } } }
      },
      "CommunityEntities": {
        "description": "Official/provider-backed entity records.",
        "content": { "application/json": { "schema": { "$ref": "./schemas/v1/community-entities.schema.json" } } }
      },
      "LocalAnswers": {
        "description": "Source-aware local routing answers.",
        "content": { "application/json": { "schema": { "$ref": "./schemas/v1/local-answers.schema.json" } } }
      },
      "FreshnessAudit": {
        "description": "Freshness classification across public records.",
        "content": { "application/json": { "schema": { "$ref": "./schemas/v1/freshness-audit.schema.json" } } }
      },
      "EditorQueue": {
        "description": "Maintenance tasks, not authoritative answers.",
        "content": { "application/json": { "schema": { "$ref": "./schemas/v1/editor-queue.schema.json" } } }
      }
    }
  }
}
