{
  "openapi": "3.1.0",
  "info": {
    "title": "Royal Road Rising Stars History API",
    "description": "Historical analytics API for Royal Road's Rising Stars fiction rankings. Captures snapshots every 30 minutes of the main list plus 16 genre lists and 63 tag lists since September 2023.\n\n**Rate Limits:** 100 requests/minute (standard), 10 requests/minute (strict endpoints). Exceeding limits returns HTTP 429.\n\n**Data Coverage:** September 2023 – present. Notable gap: May 2024 – November 2024.\n\n**fiction_id:** All fiction lookups use the Royal Road external fiction ID (integer found in the URL: royalroad.com/fiction/{id}).",
    "version": "1.0.0",
    "contact": {
      "url": "https://rst.doomlabs.net"
    }
  },
  "servers": [
    {
      "url": "https://rst.doomlabs.net/api",
      "description": "Production"
    }
  ],
  "paths": {
    "/snapshots/latest": {
      "get": {
        "summary": "Get current Rising Stars rankings",
        "description": "Returns the most recent snapshot with the full top-50 rankings and deltas compared to both the immediately previous snapshot and the snapshot from ~24 hours ago.",
        "operationId": "getLatestSnapshot",
        "tags": ["Snapshots"],
        "responses": {
          "200": {
            "description": "Current snapshot with rankings and deltas",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SnapshotWithRankings" }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/snapshots": {
      "get": {
        "summary": "List snapshots",
        "description": "Returns snapshot metadata for building a timeline or selector. Supports date range filtering.",
        "operationId": "listSnapshots",
        "tags": ["Snapshots"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of snapshots to return",
            "schema": { "type": "integer", "default": 100, "minimum": 1 }
          },
          {
            "name": "valid_only",
            "in": "query",
            "description": "If true, only return complete snapshots with exactly 50 entries",
            "schema": { "type": "boolean", "default": false }
          },
          {
            "name": "start_date",
            "in": "query",
            "description": "Filter to snapshots on or after this date (YYYY-MM-DD)",
            "schema": { "type": "string", "format": "date", "example": "2024-01-01" }
          },
          {
            "name": "end_date",
            "in": "query",
            "description": "Filter to snapshots on or before this date (YYYY-MM-DD)",
            "schema": { "type": "string", "format": "date", "example": "2024-12-31" }
          }
        ],
        "responses": {
          "200": {
            "description": "List of snapshots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "snapshots": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/SnapshotMeta" }
                    },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/snapshots/closest": {
      "get": {
        "summary": "Get snapshot closest to a timestamp",
        "description": "Finds and returns the snapshot whose capture time is nearest to the requested timestamp, along with full rankings and deltas.",
        "operationId": "getClosestSnapshot",
        "tags": ["Snapshots"],
        "parameters": [
          {
            "name": "time",
            "in": "query",
            "required": true,
            "description": "Target timestamp in 'YYYY-MM-DD HH:MM:SS' format (e.g. '2024-06-15 14:30:00')",
            "schema": { "type": "string", "example": "2024-06-15 14:30:00" }
          }
        ],
        "responses": {
          "200": {
            "description": "Snapshot closest to the requested time with rankings",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/SnapshotWithRankings" },
                    {
                      "type": "object",
                      "properties": {
                        "requested_time": { "type": "string", "description": "The timestamp you requested" }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/snapshots/compare": {
      "get": {
        "summary": "Compare two snapshots",
        "description": "Returns a diff between two snapshots: fictions that are new, fictions that dropped off, and the top 10 rank movers in each direction.",
        "operationId": "compareSnapshots",
        "tags": ["Snapshots"],
        "parameters": [
          {
            "name": "snapshot1",
            "in": "query",
            "required": true,
            "description": "ID of the older snapshot",
            "schema": { "type": "integer", "example": 4800 }
          },
          {
            "name": "snapshot2",
            "in": "query",
            "required": true,
            "description": "ID of the newer snapshot",
            "schema": { "type": "integer", "example": 4850 }
          }
        ],
        "responses": {
          "200": {
            "description": "Comparison result",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SnapshotComparison" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/snapshots/timeseries": {
      "get": {
        "summary": "Get time-bucketed snapshots for charting",
        "description": "Returns one snapshot per time bucket (hour or day) within the requested date range. Useful for building time-series charts with reduced data points.",
        "operationId": "getSnapshotTimeseries",
        "tags": ["Snapshots"],
        "parameters": [
          {
            "name": "bucket",
            "in": "query",
            "required": true,
            "description": "Time bucket size",
            "schema": { "type": "string", "enum": ["hourly", "daily"] }
          },
          {
            "name": "start_date",
            "in": "query",
            "required": true,
            "description": "Start of range (YYYY-MM-DD)",
            "schema": { "type": "string", "format": "date", "example": "2024-01-01" }
          },
          {
            "name": "end_date",
            "in": "query",
            "required": true,
            "description": "End of range (YYYY-MM-DD)",
            "schema": { "type": "string", "format": "date", "example": "2024-01-31" }
          }
        ],
        "responses": {
          "200": {
            "description": "Time-bucketed snapshots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bucket": { "type": "string", "enum": ["hourly", "daily"] },
                    "start_date": { "type": "string" },
                    "end_date": { "type": "string" },
                    "snapshots": { "type": "array", "items": { "$ref": "#/components/schemas/SnapshotMeta" } },
                    "total": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/snapshots/{id}": {
      "get": {
        "summary": "Get a specific snapshot by ID",
        "description": "Returns a specific snapshot by its numeric ID with full rankings and deltas from the previous snapshot.",
        "operationId": "getSnapshotById",
        "tags": ["Snapshots"],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Snapshot ID",
            "schema": { "type": "integer", "example": 4821 }
          }
        ],
        "responses": {
          "200": {
            "description": "Snapshot with rankings and deltas",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SnapshotWithRankings" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/snapshots/trigger": {
      "post": {
        "summary": "Manually trigger a snapshot capture",
        "description": "Immediately triggers the scraper to capture a new snapshot. Requires bearer token authentication.",
        "operationId": "triggerSnapshot",
        "tags": ["Admin"],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Snapshot triggered successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "message": { "type": "string" },
                    "snapshot_id": { "type": "integer" },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "new_fictions": { "type": "integer" },
                        "updated_fictions": { "type": "integer" },
                        "total_entries": { "type": "integer" },
                        "genres_scraped": { "type": "integer" },
                        "genre_entries": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/fictions/{fiction_id}/history": {
      "get": {
        "summary": "Get ranking history for a fiction",
        "description": "Returns the complete Rising Stars ranking history for a fiction — every snapshot it appeared in — along with stats: best rank, worst rank, total appearances, and trend direction.",
        "operationId": "getFictionHistory",
        "tags": ["Fictions"],
        "parameters": [
          {
            "name": "fiction_id",
            "in": "path",
            "required": true,
            "description": "Royal Road external fiction ID (found in the URL: royalroad.com/fiction/{id})",
            "schema": { "type": "integer", "example": 138753 }
          }
        ],
        "responses": {
          "200": {
            "description": "Fiction ranking history and stats",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/FictionHistory" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/fictions/{fiction_id}/full-stats": {
      "get": {
        "summary": "Download complete hourly stats as CSV",
        "description": "Downloads a CSV file of hourly statistics for a fiction over its entire tracked history. Columns: timestamp, views, followers, favorites, rs_pos.",
        "operationId": "getFictionStatsCsv",
        "tags": ["Fictions"],
        "parameters": [
          {
            "name": "fiction_id",
            "in": "path",
            "required": true,
            "description": "Royal Road external fiction ID",
            "schema": { "type": "integer", "example": 138753 }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV file download",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV with columns: timestamp, views, followers, favorites, rs_pos"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/check-rising-stars": {
      "get": {
        "summary": "Check which Rising Stars lists a fiction appears on",
        "description": "Checks all 79 Rising Stars lists (main + 16 genres + 63 tags) for the given fiction and returns its position on each. Data sourced from a materialized view refreshed hourly.",
        "operationId": "checkRisingStars",
        "tags": ["Fictions"],
        "parameters": [
          {
            "name": "fictionId",
            "in": "query",
            "required": true,
            "description": "Royal Road external fiction ID",
            "schema": { "type": "integer", "example": 138753 }
          }
        ],
        "responses": {
          "200": {
            "description": "Genre and tag membership results",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RisingStarsCheck" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/analytics/churn": {
      "get": {
        "summary": "Get Rising Stars churn rate over time",
        "description": "Calculates the churn rate (percentage of new entries per snapshot cycle) for each day in the requested window. High churn means a volatile, competitive list.",
        "operationId": "getChurnRate",
        "tags": ["Analytics"],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "Number of days to analyze",
            "schema": { "type": "integer", "default": 30, "minimum": 1 }
          },
          {
            "name": "reference_date",
            "in": "query",
            "description": "ISO 8601 timestamp to calculate backwards from (defaults to now)",
            "schema": { "type": "string", "format": "date-time", "example": "2024-06-15T00:00:00Z" }
          }
        ],
        "responses": {
          "200": {
            "description": "Churn rate analysis",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChurnAnalysis" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/hall-of-fame": {
      "get": {
        "summary": "Get Hall of Fame for a quarterly window",
        "description": "Returns the top 50 Legends (longest duration on Rising Stars) and top 50 Titans (highest peak followers) for a given calendar quarter. Defaults to the most recent quarter with data.",
        "operationId": "getHallOfFame",
        "tags": ["Analytics"],
        "parameters": [
          {
            "name": "window",
            "in": "query",
            "description": "Quarterly window ID in YYYY-QX format (e.g. 2024-Q1). Defaults to most recent quarter. Use available_windows in the response to discover valid values.",
            "schema": { "type": "string", "example": "2024-Q1" }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum entries per category (legends and titans). Max 100.",
            "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 100 }
          }
        ],
        "responses": {
          "200": {
            "description": "Hall of Fame data for the selected quarter",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "example": "2024-Q1" },
                        "label": { "type": "string", "example": "Q1 2024" },
                        "start_date": { "type": "string", "format": "date-time" },
                        "end_date": { "type": "string", "format": "date-time" }
                      }
                    },
                    "available_windows": {
                      "type": "array",
                      "description": "All quarters with data, most recent first",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "label": { "type": "string" },
                          "start_date": { "type": "string", "format": "date-time" },
                          "end_date": { "type": "string", "format": "date-time" }
                        }
                      }
                    },
                    "legends": {
                      "type": "array",
                      "description": "Top fictions ranked by duration on the Rising Stars list",
                      "items": { "$ref": "#/components/schemas/HallOfFameEntry" }
                    },
                    "titans": {
                      "type": "array",
                      "description": "Top fictions ranked by peak follower count",
                      "items": { "$ref": "#/components/schemas/HallOfFameEntry" }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/cohorts": {
      "get": {
        "summary": "Get publishing cohort analysis",
        "description": "Finds all fictions published within ±15 days of a reference date (or a fiction's publish date) and returns their performance stats. Useful for comparing fictions that debuted at the same time.",
        "operationId": "getCohorts",
        "tags": ["Analytics"],
        "parameters": [
          {
            "name": "fictionId",
            "in": "query",
            "description": "Royal Road fiction ID. Uses that fiction's first_published_date as the reference date.",
            "schema": { "type": "integer", "example": 138753 }
          },
          {
            "name": "date",
            "in": "query",
            "description": "Reference date in YYYY-MM-DD format. Returns fictions published within ±15 days of this date.",
            "schema": { "type": "string", "format": "date", "example": "2024-03-15" }
          }
        ],
        "responses": {
          "200": {
            "description": "Cohort fictions and aggregate stats",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reference_date": { "type": "string", "format": "date" },
                    "reference_fiction": {
                      "nullable": true,
                      "type": "object",
                      "description": "The fiction used as the reference point (only present when querying by fictionId)",
                      "properties": {
                        "external_id": { "type": "integer" },
                        "title": { "type": "string" },
                        "url": { "type": "string" },
                        "cover_url": { "type": "string", "nullable": true },
                        "first_published_date": { "type": "string", "format": "date" }
                      }
                    },
                    "cohort": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/CohortEntry" }
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "total_fictions": { "type": "integer" },
                        "avg_peak_rank": { "type": "integer", "nullable": true },
                        "avg_max_followers": { "type": "integer", "nullable": true },
                        "date_range": {
                          "type": "object",
                          "properties": {
                            "start": { "type": "string", "format": "date" },
                            "end": { "type": "string", "format": "date" }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": {
            "description": "Fiction found but has no first_published_date in the database",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "System health check",
        "description": "Returns operational status including database connectivity, latest snapshot age, snapshot counts, fiction count, and memory usage.",
        "operationId": "getHealth",
        "tags": ["System"],
        "responses": {
          "200": {
            "description": "System is healthy or degraded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthStatus" }
              }
            }
          },
          "503": {
            "description": "System is unhealthy (database error or critical failure)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HealthStatus" }
              }
            }
          }
        }
      }
    },
    "/metrics": {
      "get": {
        "summary": "Prometheus-format metrics",
        "description": "Returns operational metrics in Prometheus text format. Covers snapshot counts, snapshot age, fiction count, ranking entry count, database pool stats, memory, and uptime.",
        "operationId": "getMetrics",
        "tags": ["System"],
        "responses": {
          "200": {
            "description": "Prometheus metrics",
            "content": {
              "text/plain": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Required only for POST /snapshots/trigger. Token must match the TRIGGER_SECRET environment variable."
      }
    },
    "schemas": {
      "SnapshotMeta": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "description": "Snapshot ID" },
          "captured_at": { "type": "string", "format": "date-time" },
          "total_items": { "type": "integer", "description": "Number of ranking entries in snapshot (usually 50)" },
          "source": { "type": "string" },
          "display_name": { "type": "string", "description": "Human-readable label (e.g. 'Today 2:10 PM')" },
          "category": { "type": "string", "enum": ["Current", "Recent", "Historical"] }
        }
      },
      "RankingEntry": {
        "type": "object",
        "properties": {
          "rank": { "type": "integer", "description": "Current rank (1–50)" },
          "title": { "type": "string" },
          "url": { "type": "string", "description": "Royal Road URL for the fiction" },
          "cover_url": { "type": "string", "nullable": true },
          "tags": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "Genre/tag labels from Royal Road" },
          "fiction_id": { "type": "integer", "description": "Royal Road external fiction ID" },
          "followers": { "type": "integer" },
          "views": { "type": "integer" },
          "first_seen": { "type": "string", "format": "date-time", "description": "When this fiction first appeared in any tracked snapshot" },
          "previous_rank": { "type": "integer", "nullable": true },
          "previous_followers": { "type": "integer", "nullable": true },
          "previous_views": { "type": "integer", "nullable": true },
          "rank_delta": { "type": "integer", "nullable": true, "description": "Rank change vs previous snapshot (positive = improved)" },
          "followers_delta": { "type": "integer", "nullable": true },
          "views_delta": { "type": "integer", "nullable": true },
          "rank_24h": { "type": "integer", "nullable": true, "description": "Rank from ~24 hours ago (raw value)" },
          "rank_24h_delta": { "type": "integer", "nullable": true, "description": "Rank change vs ~24 hours ago (positive = improved)" },
          "followers_24h_delta": { "type": "integer", "nullable": true }
        }
      },
      "SnapshotWithRankings": {
        "type": "object",
        "properties": {
          "snapshot": { "$ref": "#/components/schemas/SnapshotMeta" },
          "previous_snapshot": { "$ref": "#/components/schemas/SnapshotMeta" },
          "day_ago_snapshot": { "$ref": "#/components/schemas/SnapshotMeta" },
          "rankings": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/RankingEntry" }
          },
          "total": { "type": "integer" }
        }
      },
      "SnapshotComparison": {
        "type": "object",
        "properties": {
          "snapshot1": { "$ref": "#/components/schemas/SnapshotMeta" },
          "snapshot2": { "$ref": "#/components/schemas/SnapshotMeta" },
          "summary": {
            "type": "object",
            "properties": {
              "total_compared": { "type": "integer" },
              "new_entries": { "type": "integer" },
              "dropped_entries": { "type": "integer" },
              "moved_up": { "type": "integer" },
              "moved_down": { "type": "integer" },
              "unchanged": { "type": "integer" }
            }
          },
          "changes": {
            "type": "object",
            "properties": {
              "new": { "type": "array", "items": { "$ref": "#/components/schemas/RankingEntry" }, "description": "Fictions not in snapshot1 that appear in snapshot2" },
              "dropped": { "type": "array", "items": { "$ref": "#/components/schemas/RankingEntry" }, "description": "Fictions in snapshot1 that do not appear in snapshot2" },
              "moved_up": { "type": "array", "items": { "$ref": "#/components/schemas/RankingEntry" }, "description": "Top 10 rank improvements" },
              "moved_down": { "type": "array", "items": { "$ref": "#/components/schemas/RankingEntry" }, "description": "Top 10 rank declines" }
            }
          },
          "comparison": {
            "type": "array",
            "description": "Full list with old_rank, new_rank, rank_change for every fiction",
            "items": {
              "type": "object",
              "properties": {
                "fiction_id": { "type": "integer" },
                "title": { "type": "string" },
                "old_rank": { "type": "integer", "nullable": true },
                "new_rank": { "type": "integer", "nullable": true },
                "rank_change": { "type": "integer", "nullable": true },
                "old_followers": { "type": "integer", "nullable": true },
                "new_followers": { "type": "integer", "nullable": true },
                "follower_change": { "type": "integer", "nullable": true }
              }
            }
          }
        }
      },
      "FictionHistory": {
        "type": "object",
        "properties": {
          "fiction_id": { "type": "integer" },
          "title": { "type": "string" },
          "cover_url": { "type": "string", "nullable": true },
          "stats": {
            "type": "object",
            "properties": {
              "total_snapshots": { "type": "integer", "description": "Number of snapshots this fiction appeared in" },
              "current_rank": { "type": "integer", "nullable": true },
              "best_rank": { "type": "integer", "description": "Lowest rank number (best position) ever achieved" },
              "worst_rank": { "type": "integer" },
              "first_seen": { "type": "string", "format": "date-time" },
              "last_seen": { "type": "string", "format": "date-time" },
              "trend": { "type": "string", "enum": ["improving", "declining", "stable"], "description": "Compares current rank to average of last 5 appearances" }
            }
          },
          "history": {
            "type": "array",
            "description": "All ranking entries, newest first",
            "items": {
              "type": "object",
              "properties": {
                "snapshot_id": { "type": "integer" },
                "captured_at": { "type": "string", "format": "date-time" },
                "rank": { "type": "integer" },
                "followers": { "type": "integer" },
                "views": { "type": "integer" },
                "previous_rank": { "type": "integer", "nullable": true },
                "previous_followers": { "type": "integer", "nullable": true }
              }
            }
          }
        }
      },
      "RisingStarsCheck": {
        "type": "object",
        "properties": {
          "fictionInfo": {
            "type": "object",
            "properties": {
              "title": { "type": "string" },
              "url": { "type": "string" }
            }
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "genre": { "type": "string", "description": "Human-readable list name (e.g. 'Fantasy', 'LitRPG')" },
                "value": { "type": "string", "description": "Internal identifier (snake_case)" },
                "url": { "type": "string", "description": "Royal Road URL for this Rising Stars list" },
                "found": { "type": "boolean" },
                "position": { "type": "integer", "nullable": true, "description": "Current rank on this list, null if not found" }
              }
            }
          },
          "foundCount": { "type": "integer", "description": "Number of lists the fiction currently appears on" },
          "totalLists": { "type": "integer", "description": "Total lists checked (79)" },
          "lastUpdated": { "type": "string", "format": "date-time", "description": "When the underlying materialized view was last refreshed" }
        }
      },
      "ChurnAnalysis": {
        "type": "object",
        "properties": {
          "average": { "type": "number", "description": "Average daily churn rate as a percentage over the period" },
          "period_days": { "type": "integer", "description": "Number of days with valid data in the result" },
          "history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": { "type": "string", "format": "date" },
                "churn_rate": { "type": "number", "description": "Percentage of entries that were new (new_entries / total_items * 100)" },
                "new_entries": { "type": "integer" },
                "exits": { "type": "integer" }
              }
            }
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["healthy", "degraded", "unhealthy"] },
          "timestamp": { "type": "string", "format": "date-time" },
          "uptime": { "type": "number", "description": "Process uptime in seconds" },
          "checks": {
            "type": "object",
            "properties": {
              "database": {
                "type": "object",
                "properties": {
                  "status": { "type": "string" },
                  "latency_ms": { "type": "number" },
                  "error": { "type": "string", "nullable": true }
                }
              },
              "snapshots": {
                "type": "object",
                "properties": {
                  "status": { "type": "string" },
                  "latest_snapshot_id": { "type": "integer" },
                  "latest_snapshot_time": { "type": "string", "format": "date-time" },
                  "age_minutes": { "type": "number" },
                  "source": { "type": "string" },
                  "message": { "type": "string", "nullable": true }
                }
              },
              "snapshot_stats": {
                "type": "object",
                "properties": {
                  "status": { "type": "string" },
                  "total_snapshots": { "type": "integer" },
                  "live_snapshots": { "type": "integer" },
                  "historical_snapshots": { "type": "integer" }
                }
              },
              "fictions": {
                "type": "object",
                "properties": {
                  "status": { "type": "string" },
                  "total_fictions": { "type": "integer" }
                }
              },
              "memory": {
                "type": "object",
                "properties": {
                  "status": { "type": "string" },
                  "rss_mb": { "type": "number" },
                  "heap_used_mb": { "type": "number" },
                  "heap_total_mb": { "type": "number" }
                }
              }
            }
          }
        }
      },
      "HallOfFameEntry": {
        "type": "object",
        "properties": {
          "external_id": { "type": "integer" },
          "title": { "type": "string" },
          "url": { "type": "string" },
          "cover_url": { "type": "string", "nullable": true },
          "tags": { "type": "array", "items": { "type": "string" } },
          "first_seen": { "type": "string", "format": "date-time" },
          "last_seen": { "type": "string", "format": "date-time" },
          "total_snapshots": { "type": "integer" },
          "best_rank": { "type": "integer" },
          "max_followers": { "type": "integer" },
          "days_on_list": { "type": "number", "description": "Days between first and last appearance on the Rising Stars list" }
        }
      },
      "CohortEntry": {
        "type": "object",
        "properties": {
          "fiction_id": { "type": "integer" },
          "title": { "type": "string" },
          "url": { "type": "string" },
          "cover_url": { "type": "string", "nullable": true },
          "first_published_date": { "type": "string", "format": "date" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "days_from_reference": { "type": "integer", "description": "Positive = published after reference date, negative = before" },
          "peak_rank": { "type": "integer", "nullable": true },
          "max_followers": { "type": "integer", "nullable": true },
          "first_seen_on_rs": { "type": "string", "format": "date-time", "nullable": true, "description": "When this fiction first appeared on any Rising Stars list" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid input parameter",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid bearer token",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "ServerError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    }
  },
  "tags": [
    { "name": "Snapshots", "description": "Query current and historical Rising Stars snapshots" },
    { "name": "Fictions", "description": "Fiction ranking history, stats, and list membership" },
    { "name": "Analytics", "description": "Market analytics, hall of fame, cohort analysis, and trend data" },
    { "name": "System", "description": "Health checks and metrics" },
    { "name": "Admin", "description": "Authenticated admin operations" }
  ]
}
