{
  "openapi": "3.1.0",
  "info": {
    "title": "DocSimulation API",
    "version": "1.0.0",
    "summary": "Israeli Stage B (Shlav Bet) oral board exam simulator — public catalog and practice API.",
    "description": "DocSimulation (סימולטור ועדת שלב ב׳) simulates the Israeli Medical Association Stage B oral board exam in intensive care. Catalog endpoints (specialties, syllabus content, cases without answer keys, plans) are public. Practice endpoints (sessions, grading, finalize) require the user's Google session cookie. Errors are JSON objects with error/code, message, hint, docs and requestId. Human-readable reference: https://www.docsimulation.com/developers · Agent guide: https://www.docsimulation.com/llms.txt",
    "termsOfService": "https://www.docsimulation.com/terms",
    "contact": {
      "name": "FuriTech LTD",
      "email": "noam@furitech.me",
      "url": "https://www.docsimulation.com/contact"
    }
  },
  "servers": [
    {
      "url": "https://www.docsimulation.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Meta",
      "description": "Health and discovery."
    },
    {
      "name": "Catalog",
      "description": "Specialties and public exam-prep content (no auth)."
    },
    {
      "name": "Cases",
      "description": "Clinical cases and their stages, without answer keys (no auth)."
    },
    {
      "name": "Billing",
      "description": "Plans and prices."
    },
    {
      "name": "Auth",
      "description": "Google OAuth session (cookie `sid`)."
    },
    {
      "name": "Practice",
      "description": "The practice loop: start a session, answer stages, get graded, finalize. Requires a signed-in session."
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "operationId": "getHealth",
        "summary": "Service health",
        "description": "Liveness/readiness probe. Returns 200 when the database is reachable and the case catalog is non-empty, 503 otherwise.",
        "tags": [
          "Meta"
        ],
        "responses": {
          "200": {
            "description": "Healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Degraded or unreachable database.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/specialties": {
      "get": {
        "operationId": "listSpecialties",
        "summary": "List live specialties",
        "description": "Returns the specialties currently open for practice (today: intensive care, slug `icu`) with the number of active cases in each.",
        "tags": [
          "Catalog"
        ],
        "responses": {
          "200": {
            "description": "Live specialties.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Specialty"
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/specialties/{slug}/content": {
      "get": {
        "operationId": "getSpecialtyContent",
        "summary": "Public exam-prep content for a specialty",
        "description": "Syllabus overview, common mistakes, preparation tips, recommended weekly pace and FAQ for the specialty — the same content shown on /specialty/{slug}. Cached for one hour.",
        "tags": [
          "Catalog"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Specialty slug, e.g. `icu`.",
            "schema": {
              "type": "string",
              "example": "icu"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Specialty content.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SpecialtyContent"
                }
              }
            }
          },
          "404": {
            "description": "Unknown or not-yet-live specialty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/cases": {
      "get": {
        "operationId": "listCases",
        "summary": "List active cases",
        "description": "Active clinical cases for a specialty, ordered to follow the official syllabus topic sequence. Answer keys are never included.",
        "tags": [
          "Cases"
        ],
        "parameters": [
          {
            "name": "specialty",
            "in": "query",
            "required": false,
            "description": "Specialty slug (default: `icu`).",
            "schema": {
              "type": "string",
              "default": "icu"
            }
          },
          {
            "name": "track",
            "in": "query",
            "required": false,
            "description": "Learning track (default: `shlav_bet`).",
            "schema": {
              "type": "string",
              "default": "shlav_bet"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cases in syllabus order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CaseSummary"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Specialty or track outside the live scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/cases/random": {
      "get": {
        "operationId": "getRandomCase",
        "summary": "Random active case with its stages",
        "description": "Picks a random active case and returns it with all stages (examiner prompts and questions) but without the answer key or diagnosis.",
        "tags": [
          "Cases"
        ],
        "parameters": [
          {
            "name": "specialty",
            "in": "query",
            "required": false,
            "description": "Specialty slug (default: `icu`).",
            "schema": {
              "type": "string",
              "default": "icu"
            }
          },
          {
            "name": "track",
            "in": "query",
            "required": false,
            "description": "Learning track (default: `shlav_bet`).",
            "schema": {
              "type": "string",
              "default": "shlav_bet"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A case with stages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaseWithStages"
                }
              }
            }
          },
          "404": {
            "description": "No cases available for the given scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/cases/{id}": {
      "get": {
        "operationId": "getCase",
        "summary": "Case by id with its stages",
        "description": "Returns one case with its stages in order. Expected points, red flags and the diagnosis are withheld until the practice session is finalized.",
        "tags": [
          "Cases"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Case id.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaseWithStages"
                }
              }
            }
          },
          "404": {
            "description": "Case not found or outside the live scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/billing/plans": {
      "get": {
        "operationId": "listPlans",
        "summary": "Access plans and prices",
        "description": "Available plans (monthly subscription and exam pass) with prices in agorot. When called with a session cookie, the exam pass is tailored to the user's exam date.",
        "tags": [
          "Billing"
        ],
        "responses": {
          "200": {
            "description": "Plans.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlansResponse"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/tts": {
      "post": {
        "operationId": "synthesizeSpeech",
        "summary": "Hebrew text-to-speech (MP3)",
        "description": "Reads text aloud in the examiner's voice and returns audio/mpeg. Rate-limited per IP (60 requests / 15 min). Returns 503 when speech synthesis is not configured.",
        "tags": [
          "Practice"
        ],
        "requestBody": {
          "required": true,
          "description": "Text to synthesize.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "Text to read aloud (1–8000 characters).",
                    "minLength": 1,
                    "maxLength": 8000
                  },
                  "gender": {
                    "type": "string",
                    "description": "Examiner voice.",
                    "enum": [
                      "male",
                      "female"
                    ]
                  }
                }
              },
              "example": {
                "text": "שלום, נתחיל במקרה הראשון.",
                "gender": "female"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "MP3 audio (audio/mpeg).",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Speech synthesis not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/auth/google": {
      "get": {
        "operationId": "startGoogleLogin",
        "summary": "Start Google sign-in",
        "description": "Browser flow: redirects to Google OAuth. On success a `sid` session cookie is set and the browser returns to the app. This is the only way to obtain a session; there are no API keys.",
        "tags": [
          "Auth"
        ],
        "responses": {
          "302": {
            "description": "Redirect to Google."
          }
        },
        "security": []
      }
    },
    "/api/auth/me": {
      "get": {
        "operationId": "getCurrentUser",
        "summary": "Current user",
        "description": "Returns the signed-in user, or 401 when there is no valid session cookie.",
        "tags": [
          "Auth"
        ],
        "responses": {
          "200": {
            "description": "The user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/api/auth/logout": {
      "post": {
        "operationId": "logout",
        "summary": "Sign out",
        "description": "Destroys the session and clears the cookie.",
        "tags": [
          "Auth"
        ],
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Always true."
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/api/sessions": {
      "post": {
        "operationId": "startSession",
        "summary": "Start a practice session on a case",
        "description": "Creates a session for the given case. On a free trial this consumes one credit (402 when exhausted; `upgradeUrl` points to the purchase page).",
        "tags": [
          "Practice"
        ],
        "requestBody": {
          "required": true,
          "description": "Which case to practise.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "caseId"
                ],
                "properties": {
                  "caseId": {
                    "type": "integer",
                    "description": "Case id.",
                    "minimum": 1
                  },
                  "retryOfSessionId": {
                    "type": "integer",
                    "description": "Re-practise: id of a previous session on the same case (does not consume a credit).",
                    "minimum": 1
                  },
                  "mockExamId": {
                    "type": "integer",
                    "description": "Exam-day mode: the mock exam this case belongs to.",
                    "minimum": 1
                  }
                }
              },
              "example": {
                "caseId": 12
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Trial exhausted or subscription expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Case not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      },
      "get": {
        "operationId": "listSessions",
        "summary": "The user's practice history",
        "description": "All sessions of the signed-in user, newest first.",
        "tags": [
          "Practice"
        ],
        "responses": {
          "200": {
            "description": "Sessions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Session"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/api/grade": {
      "post": {
        "operationId": "gradeStageAnswer",
        "summary": "Grade an answer for one stage",
        "description": "Submits the candidate's answer for a stage and returns structured examiner feedback (covered / missed / red flags / model answer / follow-up). Rate-limited (30 / 15 min).",
        "tags": [
          "Practice"
        ],
        "requestBody": {
          "required": true,
          "description": "The answer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sessionId",
                  "stageOrder",
                  "answer"
                ],
                "properties": {
                  "sessionId": {
                    "type": "integer",
                    "description": "Session id.",
                    "minimum": 1
                  },
                  "stageOrder": {
                    "type": "integer",
                    "description": "1-based stage order.",
                    "minimum": 1,
                    "maximum": 20
                  },
                  "answer": {
                    "type": "string",
                    "description": "The candidate's answer (Hebrew or English, 1–20000 chars).",
                    "minLength": 1,
                    "maxLength": 20000
                  },
                  "meta": {
                    "type": "object",
                    "description": "Optional answer metadata.",
                    "properties": {
                      "durationMs": {
                        "type": "integer",
                        "description": "Time spent answering, ms."
                      },
                      "inputMode": {
                        "type": "string",
                        "description": "How the answer was given.",
                        "enum": [
                          "voice",
                          "text"
                        ]
                      },
                      "wordCount": {
                        "type": "integer",
                        "description": "Word count."
                      }
                    }
                  }
                }
              },
              "example": {
                "sessionId": 101,
                "stageOrder": 1,
                "answer": "אתחיל ב-ABC, אבטח נתיב אוויר ואבדוק המודינמיקה...",
                "meta": {
                  "inputMode": "text"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Structured feedback.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GradeResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/api/grade/followup": {
      "post": {
        "operationId": "gradeFollowUpAnswer",
        "summary": "Answer the examiner's follow-up question",
        "description": "Second chance on a missed critical point: grades the follow-up answer for the given stage.",
        "tags": [
          "Practice"
        ],
        "requestBody": {
          "required": true,
          "description": "The follow-up answer.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sessionId",
                  "stageOrder",
                  "answer"
                ],
                "properties": {
                  "sessionId": {
                    "type": "integer",
                    "description": "Session id.",
                    "minimum": 1
                  },
                  "stageOrder": {
                    "type": "integer",
                    "description": "1-based stage order.",
                    "minimum": 1,
                    "maximum": 20
                  },
                  "answer": {
                    "type": "string",
                    "description": "The follow-up answer (1–8000 chars).",
                    "minLength": 1,
                    "maxLength": 8000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated feedback.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GradeResult"
                }
              }
            }
          },
          "400": {
            "description": "Missing fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/api/sessions/{id}/finalize": {
      "post": {
        "operationId": "finalizeSession",
        "summary": "Finish a session and reveal the diagnosis",
        "description": "Computes totals server-side from the graded stages, reveals the diagnosis and updates XP/streak/badges. In exam-day mode it advances the mock exam.",
        "tags": [
          "Practice"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Session id.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session totals.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FinalizeResult"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Session not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/api/sessions/{id}": {
      "get": {
        "operationId": "getSession",
        "summary": "Session detail with per-stage answers and feedback",
        "description": "One session of the signed-in user, including the answer and feedback stored for each stage.",
        "tags": [
          "Practice"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Session id.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session detail.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Session not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "description": "Structured error. `error`/`code` is a stable machine-readable code, `message` explains the failure in plain language, `hint` says how to resolve it and `docs` links the API reference.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable error code (same value as `code`).",
            "example": "not_found"
          },
          "code": {
            "type": "string",
            "description": "Stable error code.",
            "example": "not_found"
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          },
          "hint": {
            "type": "string",
            "description": "How to resolve the error."
          },
          "docs": {
            "type": "string",
            "description": "URL of the API reference.",
            "format": "uri"
          },
          "requestId": {
            "type": "string",
            "description": "Request id echoed from the X-Request-Id header, for support."
          },
          "upgradeUrl": {
            "type": "string",
            "description": "Present on 402 responses: where the user can purchase access."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present on validation errors: the invalid field paths."
          }
        },
        "additionalProperties": true
      },
      "Health": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when the database is reachable and the case catalog is non-empty."
          },
          "activeCases": {
            "type": "integer",
            "description": "Number of active clinical cases."
          },
          "medicalChunks": {
            "type": "integer",
            "description": "Number of indexed reading-material chunks used for grounded feedback."
          },
          "openai": {
            "type": "boolean",
            "description": "Whether an OpenAI key is configured (grading/TTS available)."
          },
          "error": {
            "type": "string",
            "description": "Present when ok=false."
          }
        }
      },
      "Specialty": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "nameHe",
          "nameEn",
          "format",
          "caseCount"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Internal id."
          },
          "slug": {
            "type": "string",
            "description": "URL slug, e.g. `icu`.",
            "example": "icu"
          },
          "nameHe": {
            "type": "string",
            "description": "Hebrew name."
          },
          "nameEn": {
            "type": "string",
            "description": "English name."
          },
          "description": {
            "type": "string",
            "description": "Short description (Hebrew)."
          },
          "format": {
            "type": "string",
            "description": "Exam format.",
            "enum": [
              "rolling_case",
              "image_reading",
              "slide_reading",
              "scan_interpretation",
              "clinical_image",
              "osce_stations",
              "written_scenario"
            ]
          },
          "formatNotes": {
            "type": "string",
            "description": "How the oral exam is structured for this specialty."
          },
          "icon": {
            "type": "string",
            "description": "lucide icon name used by the UI."
          },
          "sortOrder": {
            "type": "integer",
            "description": "Display order."
          },
          "isActive": {
            "type": "boolean",
            "description": "Whether the specialty is live."
          },
          "caseCount": {
            "type": "integer",
            "description": "Number of active cases available for practice."
          }
        }
      },
      "SpecialtyTopic": {
        "type": "object",
        "required": [
          "label",
          "core",
          "freqLabel"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "Syllabus topic (Hebrew)."
          },
          "core": {
            "type": "boolean",
            "description": "True for core syllabus topics."
          },
          "freqLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Estimated exam frequency label (Hebrew) or null."
          }
        }
      },
      "SpecialtyContent": {
        "type": "object",
        "description": "Public exam-prep content for a specialty: intro, syllabus topics, common mistakes, tips, pace and FAQ.",
        "required": [
          "intro",
          "formatNotes",
          "topicCount",
          "topics",
          "mistakes",
          "tips",
          "pace",
          "faq"
        ],
        "properties": {
          "intro": {
            "type": "string",
            "description": "Intro paragraph (Hebrew)."
          },
          "formatNotes": {
            "type": "string",
            "description": "Exam format notes (Hebrew)."
          },
          "topicCount": {
            "type": "integer",
            "description": "Number of syllabus topics."
          },
          "topics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SpecialtyTopic"
            }
          },
          "mistakes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Common mistakes candidates make."
          },
          "tips": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Preparation tips."
          },
          "pace": {
            "type": [
              "object",
              "null"
            ],
            "description": "Recommended weekly number of cases, or null.",
            "properties": {
              "low": {
                "type": "integer",
                "description": "Cases/week, light pace."
              },
              "medium": {
                "type": "integer",
                "description": "Cases/week, medium pace."
              },
              "intensive": {
                "type": "integer",
                "description": "Cases/week, intensive pace."
              }
            }
          },
          "faq": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "q",
                "a"
              ],
              "properties": {
                "q": {
                  "type": "string",
                  "description": "Question."
                },
                "a": {
                  "type": "string",
                  "description": "Answer."
                }
              }
            }
          }
        }
      },
      "SourceRef": {
        "type": "object",
        "description": "Citation into the exam reading material.",
        "properties": {
          "sourceFile": {
            "type": "string",
            "description": "Source document name."
          },
          "pageStart": {
            "type": "integer",
            "description": "First page."
          },
          "pageEnd": {
            "type": "integer",
            "description": "Last page."
          },
          "heading": {
            "type": "string",
            "description": "Section heading, when known."
          }
        }
      },
      "CaseSummary": {
        "type": "object",
        "description": "A clinical case as listed on the specialty page (no answer key, no diagnosis).",
        "required": [
          "id",
          "slug",
          "specialty",
          "track",
          "format",
          "title",
          "topic",
          "oneLiner",
          "difficulty",
          "isActive"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Case id."
          },
          "slug": {
            "type": "string",
            "description": "Stable case slug."
          },
          "specialty": {
            "type": "string",
            "description": "Specialty slug.",
            "example": "icu"
          },
          "track": {
            "type": "string",
            "description": "Learning track.",
            "example": "shlav_bet"
          },
          "format": {
            "type": "string",
            "description": "Case format.",
            "example": "rolling_case"
          },
          "title": {
            "type": "string",
            "description": "Case title (Hebrew)."
          },
          "topic": {
            "type": "string",
            "description": "Syllabus topic (Hebrew)."
          },
          "topicKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "Syllabus topic key."
          },
          "diagnosis": {
            "type": "string",
            "description": "Final diagnosis — only present in the listing; hidden from the practice payload until the session is finalized."
          },
          "oneLiner": {
            "type": "string",
            "description": "One-line case summary."
          },
          "difficulty": {
            "type": "string",
            "description": "Difficulty (Hebrew): בינוני / מתקדם / קשה."
          },
          "mediaUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional image URL."
          },
          "sourceRefs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SourceRef"
            }
          },
          "conceptKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "Groups variants of the same scenario."
          },
          "reviewedBy": {
            "type": [
              "string",
              "null"
            ],
            "description": "Clinical reviewer name, when reviewed."
          },
          "reviewedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "isActive": {
            "type": "boolean",
            "description": "Whether the case is live."
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time.",
            "format": "date-time"
          }
        }
      },
      "CaseStagePublic": {
        "type": "object",
        "description": "One stage of a rolling case, without the answer key (expected points and red flags stay server-side).",
        "required": [
          "id",
          "caseId",
          "stageOrder",
          "title",
          "examinerPrompt",
          "question",
          "expectedCount",
          "criticalCount"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stage id."
          },
          "caseId": {
            "type": "integer",
            "description": "Parent case id."
          },
          "stageOrder": {
            "type": "integer",
            "description": "1-based stage order."
          },
          "title": {
            "type": "string",
            "description": "Stage title."
          },
          "examinerPrompt": {
            "type": "string",
            "description": "What the examiner reveals/says at this stage (Hebrew)."
          },
          "context": {
            "type": [
              "string",
              "null"
            ],
            "description": "Additional revealed data (labs, imaging)."
          },
          "question": {
            "type": "string",
            "description": "The question posed to the candidate."
          },
          "expectedCount": {
            "type": "integer",
            "description": "Number of expected answer points."
          },
          "criticalCount": {
            "type": "integer",
            "description": "Number of critical (must-mention) points."
          },
          "findings": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Structured findings (vitals, ABG, labs)."
          },
          "questionVariants": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sourceRefs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SourceRef"
            }
          },
          "mediaUrl": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CaseWithStages": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CaseSummary"
          },
          {
            "type": "object",
            "required": [
              "stages"
            ],
            "properties": {
              "stages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/CaseStagePublic"
                },
                "description": "Stages in order. `diagnosis` is omitted from this payload."
              }
            }
          }
        ]
      },
      "Plan": {
        "type": "object",
        "required": [
          "id",
          "priceAgrot",
          "accessDays"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Plan id.",
            "enum": [
              "monthly",
              "exam_pass"
            ]
          },
          "name": {
            "type": "string",
            "description": "Display name (Hebrew)."
          },
          "priceAgrot": {
            "type": "integer",
            "description": "Price in agorot (ILS × 100)."
          },
          "accessDays": {
            "type": "integer",
            "description": "Days of access granted."
          },
          "accessUntil": {
            "type": "string",
            "description": "Exam-pass end date.",
            "format": "date-time"
          },
          "examDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The signed-in user's exam date, if set."
          },
          "recommended": {
            "type": "boolean",
            "description": "Whether the exam pass is the recommended plan for this user."
          }
        },
        "additionalProperties": true
      },
      "PlansResponse": {
        "type": "object",
        "required": [
          "plans"
        ],
        "properties": {
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Plan"
            }
          }
        }
      },
      "User": {
        "type": "object",
        "description": "The signed-in user (safe subset).",
        "required": [
          "id",
          "email",
          "fullName",
          "role",
          "subscriptionStatus",
          "trialQuestionsRemaining"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "User id (UUID)."
          },
          "email": {
            "type": "string",
            "description": "Email.",
            "format": "email"
          },
          "fullName": {
            "type": "string",
            "description": "Display name."
          },
          "profileImage": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": "string",
            "description": "Role.",
            "enum": [
              "user",
              "admin"
            ]
          },
          "subscriptionStatus": {
            "type": "string",
            "description": "Subscription state.",
            "enum": [
              "trial",
              "active",
              "cancelled",
              "expired",
              "past_due"
            ]
          },
          "subscriptionEndDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "trialQuestionsRemaining": {
            "type": "integer",
            "description": "Free cases left on the trial."
          },
          "targetTrack": {
            "type": [
              "string",
              "null"
            ]
          },
          "targetSpecialty": {
            "type": [
              "string",
              "null"
            ]
          },
          "targetExamDate": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "Session": {
        "type": "object",
        "description": "A practice session on one case.",
        "required": [
          "id",
          "caseId",
          "caseTitle",
          "mode",
          "isCompleted",
          "totalStages"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Session id."
          },
          "caseId": {
            "type": "integer",
            "description": "Case id."
          },
          "caseTitle": {
            "type": "string",
            "description": "Case title."
          },
          "mode": {
            "type": "string",
            "description": "practice or exam (exam-day mode).",
            "enum": [
              "practice",
              "exam"
            ]
          },
          "mockExamId": {
            "type": [
              "integer",
              "null"
            ]
          },
          "totalScore": {
            "type": "integer",
            "description": "Points earned so far."
          },
          "totalPossible": {
            "type": "integer",
            "description": "Points possible so far."
          },
          "passedStages": {
            "type": "integer",
            "description": "Stages passed."
          },
          "totalStages": {
            "type": "integer",
            "description": "Stages in the case."
          },
          "isCompleted": {
            "type": "boolean",
            "description": "True once finalized."
          },
          "chargedCredit": {
            "type": "boolean",
            "description": "True when starting this session consumed a free-trial credit."
          }
        },
        "additionalProperties": true
      },
      "GradePoint": {
        "type": "object",
        "required": [
          "point",
          "critical"
        ],
        "properties": {
          "point": {
            "type": "string",
            "description": "Expected point."
          },
          "critical": {
            "type": "boolean",
            "description": "Whether it is critical."
          }
        }
      },
      "GradeResult": {
        "type": "object",
        "description": "Structured examiner feedback for one stage answer.",
        "required": [
          "covered",
          "missed",
          "extras",
          "redFlagsTriggered",
          "score",
          "total",
          "summary",
          "passed"
        ],
        "properties": {
          "covered": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GradePoint"
            },
            "description": "Expected points the answer covered."
          },
          "missed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GradePoint"
            },
            "description": "Expected points the answer missed."
          },
          "extras": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Correct points beyond the key."
          },
          "redFlagsTriggered": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Dangerous statements the answer contained."
          },
          "score": {
            "type": "integer",
            "description": "Points earned."
          },
          "total": {
            "type": "integer",
            "description": "Points possible."
          },
          "summary": {
            "type": "string",
            "description": "Examiner summary (Hebrew)."
          },
          "passed": {
            "type": "boolean",
            "description": "Whether the stage passed."
          },
          "modelAnswer": {
            "type": "string",
            "description": "How a strong candidate would answer (Hebrew, spoken style)."
          },
          "technique": {
            "type": "object",
            "description": "Feedback on exam technique (structure, prioritisation)."
          },
          "followUp": {
            "type": "object",
            "description": "Examiner follow-up question when a critical point was missed."
          },
          "passedAfterFollowUp": {
            "type": "boolean",
            "description": "Pass state after the follow-up answer."
          },
          "sources": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SourceRef"
            },
            "description": "Reading-material citations backing the feedback."
          }
        }
      },
      "FinalizeResult": {
        "type": "object",
        "required": [
          "ok",
          "diagnosis",
          "totalScore",
          "totalPossible",
          "passedStages",
          "totalStages"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Always true on success."
          },
          "diagnosis": {
            "type": "string",
            "description": "The case's final diagnosis, revealed on completion."
          },
          "totalScore": {
            "type": "integer",
            "description": "Total points earned."
          },
          "totalPossible": {
            "type": "integer",
            "description": "Total points possible."
          },
          "passedStages": {
            "type": "integer",
            "description": "Stages passed."
          },
          "totalStages": {
            "type": "integer",
            "description": "Stages in the case."
          },
          "mockExam": {
            "type": "object",
            "description": "Present in exam-day mode: next case id or done flag."
          },
          "stats": {
            "type": "object",
            "description": "XP, streak and badges after this session."
          }
        }
      }
    },
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "sid",
        "description": "Session cookie issued after Google OAuth sign-in at /api/auth/google. No API keys exist."
      }
    }
  },
  "externalDocs": {
    "description": "Developer & agent documentation",
    "url": "https://www.docsimulation.com/developers"
  }
}