{
  "openapi": "3.1.0",
  "info": {
    "title": "BedrockNews API",
    "version": "1.0",
    "description": "Open, read-only REST API for BedrockNews — a solutions-focused news platform. Every story ships with a machine-readable GRIN/CLAIMS/NOVEL editorial decomposition, a 0-100 Plumb clarity score, and a re-renderable extraction graph. No authentication is required. GRIN/CLAIMS/NOVEL is a disclosed editorial lens, not neutral fact — see /terms and /agents.txt.",
    "contact": {
      "name": "BedrockNews",
      "email": "legal@bedrocknews.com",
      "url": "https://bedrocknews.com"
    },
    "license": {
      "name": "Terms of Service",
      "url": "https://bedrocknews.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.bedrocknews.com",
      "description": "Production API"
    }
  ],
  "tags": [
    { "name": "Articles", "description": "Story discovery and retrieval." },
    { "name": "Extraction graph", "description": "Nodes + edges extraction maps." },
    { "name": "Categories", "description": "The BedrockNews category taxonomy." }
  ],
  "paths": {
    "/api/articles": {
      "get": {
        "operationId": "listArticles",
        "summary": "List / discover articles",
        "description": "Cursor-paginated, newest-first article feed with GRIN-aware discovery filters.",
        "tags": ["Articles"],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "BedrockNews category slug (e.g. politics, ai). Omit or \"all\" for everything.",
            "schema": { "type": "string" }
          },
          {
            "name": "since",
            "in": "query",
            "description": "ISO 8601 lower bound on publish date. Documented alias for `after`.",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "after",
            "in": "query",
            "description": "ISO 8601 lower bound on publish date.",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "before",
            "in": "query",
            "description": "ISO 8601 upper bound on publish date.",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Substring match on title.",
            "schema": { "type": "string" }
          },
          {
            "name": "verdict",
            "in": "query",
            "description": "GRIN verdict, derived from grinScores.extractive.",
            "schema": { "type": "string", "enum": ["extractive", "mixed", "generative"] }
          },
          {
            "name": "editorialMode",
            "in": "query",
            "description": "Which editorial framework produced the analysis.",
            "schema": { "type": "string", "enum": ["grin", "culture", "research", "ledger"] }
          },
          {
            "name": "plumbMin",
            "in": "query",
            "description": "Inclusive lower bound on the 0-100 Plumb (analytical clarity) score.",
            "schema": { "type": "integer", "minimum": 0, "maximum": 100 }
          },
          {
            "name": "plumbMax",
            "in": "query",
            "description": "Inclusive upper bound on the 0-100 Plumb score.",
            "schema": { "type": "integer", "minimum": 0, "maximum": 100 }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Projection. `fields=grin` returns analysis-only items (no body text), or a comma-separated subset of the light identity fields (id, title, section, categoryId, publishedAt, imageUrl, sourceUrl, status, source, newsSource). Unrecognized keys are ignored.",
            "schema": { "type": "string" }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max items per page (server caps at 50).",
            "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque pagination cursor from a previous response's `nextCursor`.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated article feed.",
            "content": {
              "application/json": {
                "schema": { "$ref": "https://bedrocknews.com/schema/v1/articles.json" }
              }
            }
          },
          "400": {
            "description": "Invalid cursor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/article/{id}": {
      "get": {
        "operationId": "getArticle",
        "summary": "Get a single article",
        "description": "Fetch one story with its full structured analysis. The `id` path segment may itself contain literal slashes (e.g. `technology/2026/sep/24/slug`) — the server matches it as a single greedy path parameter, not one segment per `/`.",
        "tags": ["Articles"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Article id, e.g. technology/2026/sep/24/slug. Contains literal slashes.",
            "schema": { "type": "string" }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "`fields=grin` (or a comma-separated subset of the identity fields) to pull just the analysis instead of the full article.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The article and its analysis.",
            "content": {
              "application/json": {
                "schema": { "$ref": "https://bedrocknews.com/schema/v1/article.json" }
              }
            }
          },
          "404": {
            "description": "Article not found.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/extraction-graph/{id}": {
      "get": {
        "operationId": "getExtractionGraph",
        "summary": "Get an extraction graph",
        "description": "A story's extraction map as real nodes + edges data — re-renderable in any style, not a screenshot. Same id-with-slashes convention as GET /api/article/{id}.",
        "tags": ["Extraction graph"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Article id, e.g. technology/2026/sep/24/slug. Contains literal slashes.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The extraction graph.",
            "content": {
              "application/json": {
                "schema": { "$ref": "https://bedrocknews.com/schema/v1/extraction-graph.json" }
              }
            }
          },
          "404": {
            "description": "Article not found, or the article has no extraction graph (non-GRIN editorial mode, or an older transform).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/articles/related": {
      "get": {
        "operationId": "getRelatedArticles",
        "summary": "Get related articles",
        "description": "Related stories for a given article (same section + recency + editorial-mode/source bonuses). `id` is a query param, not a path segment, because article ids contain slashes.",
        "tags": ["Articles"],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Source article id.",
            "schema": { "type": "string" }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max related items (server caps at 12).",
            "schema": { "type": "integer", "minimum": 1, "maximum": 12, "default": 4 }
          }
        ],
        "responses": {
          "200": {
            "description": "Related articles, feed-trimmed list items.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "count"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "https://bedrocknews.com/schema/v1/article-list-item.json" }
                    },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `id`.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "404": {
            "description": "Source article not found.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    },
    "/api/categories": {
      "get": {
        "operationId": "listCategories",
        "summary": "List categories",
        "description": "The BedrockNews category taxonomy — the single source of truth for category ids, display names, and which editorial framework each maps to.",
        "tags": ["Categories"],
        "responses": {
          "200": {
            "description": "The category taxonomy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Category" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" }
        }
      },
      "Category": {
        "type": "object",
        "required": ["id", "name", "description", "editorialMode"],
        "properties": {
          "id": { "type": "string", "description": "URL-safe slug used in query params and the DB." },
          "name": { "type": "string", "description": "Display name in nav." },
          "description": { "type": "string" },
          "icon": { "type": "string", "description": "Optional emoji or icon identifier." },
          "editorialMode": {
            "type": "string",
            "enum": ["grin", "culture", "auto"],
            "description": "Hint for which editorial framework transforms stories in this category."
          }
        }
      }
    }
  }
}
