{
  "openapi": "3.1.0",
  "info": {
    "title": "alexkachur.com API",
    "version": "b9ca030b54f5dc026859a4f70348fbf2e1fbd99c",
    "description": "These endpoints return the same records the pages are built from."
  },
  "servers": [
    {
      "url": "https://alexkachur.com"
    }
  ],
  "paths": {
    "/api/projects.json": {
      "get": {
        "operationId": "listProjects",
        "summary": "The projects on this site, in site order",
        "responses": {
          "200": {
            "description": "Every row of the projects table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/projects"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/projects/{slug}.json": {
      "get": {
        "operationId": "getProject",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The slug of the case-study page under /work/",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project and its technologies",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/project_detail"
                }
              }
            }
          },
          "404": {
            "description": "No project has this slug"
          }
        }
      }
    },
    "/api/facts.json": {
      "get": {
        "operationId": "listFacts",
        "summary": "Single facts about Alex, one per row",
        "responses": {
          "200": {
            "description": "Every row of the facts table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/facts"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/technologies.json": {
      "get": {
        "operationId": "listTechnologies",
        "summary": "Languages, frameworks, libraries and services Alex has used",
        "responses": {
          "200": {
            "description": "Every row of the technologies table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/technologies"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/project_images.json": {
      "get": {
        "operationId": "listProjectImages",
        "summary": "The screenshots on each case study, in display order",
        "responses": {
          "200": {
            "description": "Every row of the project_images table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/project_images"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/courses.json": {
      "get": {
        "operationId": "listCourses",
        "summary": "Courses Alex is taking at Centennial College",
        "responses": {
          "200": {
            "description": "Every row of the courses table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/courses"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/timeline.json": {
      "get": {
        "operationId": "listTimeline",
        "summary": "Work, education and project events by date",
        "responses": {
          "200": {
            "description": "Every row of the timeline table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/timeline"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/pets.json": {
      "get": {
        "operationId": "listPets",
        "summary": "The cats Alex lives with",
        "responses": {
          "200": {
            "description": "Every row of the pets table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/pets"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/experience.json": {
      "get": {
        "operationId": "listExperience",
        "summary": "Jobs and roles Alex has held",
        "responses": {
          "200": {
            "description": "Every row of the experience table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/experience"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/interests.json": {
      "get": {
        "operationId": "listInterests",
        "summary": "Alex's hobbies and favourites, in his words",
        "responses": {
          "200": {
            "description": "Every row of the interests table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/interests"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/storage.json": {
      "get": {
        "operationId": "listStorage",
        "summary": "What this site stores for a visitor or a question, how long, and why, from the constants the code uses",
        "responses": {
          "200": {
            "description": "Every row of the storage table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/storage"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/uses.json": {
      "get": {
        "operationId": "listUses",
        "summary": "The machines, peripherals and software Alex uses, and what he is learning, as listed on /uses",
        "responses": {
          "200": {
            "description": "Every row of the uses table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/uses"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/sections.json": {
      "get": {
        "operationId": "listSections",
        "summary": "The text of the site's pages and case studies, one row per section",
        "responses": {
          "200": {
            "description": "Every row of the sections table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/sections"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/page_images.json": {
      "get": {
        "operationId": "listPageImages",
        "summary": "The photos on the site's other pages",
        "responses": {
          "200": {
            "description": "Every row of the page_images table",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/page_images"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/schema.json": {
      "get": {
        "operationId": "getSchema",
        "responses": {
          "200": {
            "description": "The database schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/database_schema"
                }
              }
            }
          }
        }
      }
    },
    "/api/resume.json": {
      "get": {
        "operationId": "getResume",
        "description": "Alex's resume in the JSON Resume format, built from the same tables.",
        "responses": {
          "200": {
            "description": "A JSON Resume document, schema 1.x from jsonresume.org",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/ask": {
      "post": {
        "operationId": "ask",
        "description": "Rate limits: 10 questions a minute per address and a monthly cap; when the cap is reached the endpoint returns 503 with reason \"budget\".",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ask_request"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SQL for the question, or an explanation of why there is none",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ask_answer"
                }
              }
            }
          },
          "400": {
            "description": "The body has no question of 3 to 200 characters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "invalid_question"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "422": {
            "description": "The model gave nothing that prepares as a safe query",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "unusable_output"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "More than 10 questions in a minute from one address",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "rate_limited"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "An unexpected failure",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "internal"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Not answering: the monthly cap is reached, the service is not set up, or the model did not respond",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/api/questions": {
      "post": {
        "operationId": "sendQuestion",
        "description": "Stores a question the site could not answer, for Alex to read, only when the visitor chooses to send it. It needs the token /api/ask returned with that question, no older than 10 minutes, and keeps only the question and the day it was sent, for 90 days.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/send_request"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The question is kept for Alex",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/send_answer"
                }
              }
            }
          },
          "400": {
            "description": "The body has no question of 3 to 200 characters, or it holds control or text-direction characters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "invalid_question"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "403": {
            "description": "The token is missing, not for this question, or older than 10 minutes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "invalid_token"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Over the rate limit, or the day has taken all the questions it can",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "enum": [
                        "rate_limited",
                        "daily_cap"
                      ]
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "An unexpected failure",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "const": "internal"
                    }
                  },
                  "required": [
                    "error"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Not answering: asking is switched off, the service is not set up, or the store did not respond",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/api/stats": {
      "get": {
        "operationId": "getStats",
        "responses": {
          "200": {
            "description": "The counters for the current month and the build",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/stats"
                }
              }
            }
          },
          "503": {
            "description": "The counters could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/unavailable"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "facts": {
        "type": "object",
        "description": "Single facts about Alex, one per row",
        "properties": {
          "key": {
            "type": "string",
            "description": "Fact name, for example location or available_from"
          },
          "value": {
            "type": "string",
            "description": "The fact as text"
          },
          "description": {
            "type": "string",
            "description": "What the key means"
          }
        },
        "required": [
          "key",
          "value",
          "description"
        ]
      },
      "projects": {
        "type": "object",
        "description": "The projects on this site, in site order",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Position in the site order, 1 first"
          },
          "slug": {
            "type": "string",
            "description": "URL slug of the case-study page under /work/"
          },
          "name": {
            "type": "string",
            "description": "Project name"
          },
          "kind": {
            "type": "string",
            "enum": [
              "client",
              "team",
              "course",
              "personal"
            ],
            "description": "client, team, course or personal"
          },
          "summary": {
            "type": "string",
            "description": "One-line summary"
          },
          "role": {
            "type": "string",
            "description": "Alex's role on the project"
          },
          "year_start": {
            "type": "integer",
            "description": "Year the work started"
          },
          "year_end": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Year the work ended, NULL if ongoing"
          },
          "client_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Client name for client work, else NULL"
          },
          "live_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL of the live site or demo, else NULL"
          },
          "repo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL of the public repository, else NULL"
          },
          "has_live_demo": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "1 if something is live to look at"
          },
          "uses_llm": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "1 if a language model does work inside the project"
          },
          "llm_job": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the language model does, NULL when uses_llm is 0"
          },
          "paid": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "1 if the work was paid"
          },
          "featured": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "1 for the project listed first on the home page, else 0"
          },
          "card": {
            "type": "string",
            "description": "The one-liner under the name on the home page, also the case study's description"
          },
          "team": {
            "type": [
              "string",
              "null"
            ],
            "description": "The team line on the case study, NULL unless a team project"
          },
          "highlights": {
            "type": [
              "string",
              "null"
            ],
            "description": "Resume bullets for the project, one per line, NULL for client work; numbers in them are filled in at build time from their sources"
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "kind",
          "summary",
          "role",
          "year_start",
          "year_end",
          "client_name",
          "live_url",
          "repo_url",
          "has_live_demo",
          "uses_llm",
          "llm_job",
          "paid",
          "featured",
          "card",
          "team",
          "highlights"
        ]
      },
      "technologies": {
        "type": "object",
        "description": "Languages, frameworks, libraries and services Alex has used",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Position in case-insensitive name order, 1 first"
          },
          "name": {
            "type": "string",
            "description": "Canonical, unversioned name, for example React"
          },
          "category": {
            "type": "string",
            "enum": [
              "language",
              "framework",
              "library",
              "runtime",
              "database",
              "ai",
              "service",
              "testing",
              "tooling",
              "platform"
            ],
            "description": "language, framework, library, runtime, database, ai, service, testing, tooling, platform"
          },
          "core": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "Skills Alex considers core, each backed by a project on this site"
          },
          "skill_area": {
            "type": "string",
            "enum": [
              "Languages",
              "Frontend",
              "Backend",
              "Data",
              "LLM integration",
              "Testing",
              "Platforms and services"
            ],
            "description": "Languages, Frontend, Backend, Data, LLM integration, Testing, or Platforms and services"
          }
        },
        "required": [
          "id",
          "name",
          "category",
          "core",
          "skill_area"
        ]
      },
      "project_technologies": {
        "type": "object",
        "description": "Which technology was used on which project",
        "properties": {
          "project_id": {
            "type": "integer",
            "description": "projects.id"
          },
          "technology_id": {
            "type": "integer",
            "description": "technologies.id"
          }
        },
        "required": [
          "project_id",
          "technology_id"
        ]
      },
      "project_images": {
        "type": "object",
        "description": "The screenshots on each case study, in display order",
        "properties": {
          "project_id": {
            "type": "integer",
            "description": "projects.id"
          },
          "position": {
            "type": "integer",
            "description": "Order on the page, 1 first"
          },
          "alt": {
            "type": "string",
            "description": "What the screenshot shows"
          },
          "caption": {
            "type": [
              "string",
              "null"
            ],
            "description": "Caption under the screenshot, NULL until written"
          }
        },
        "required": [
          "project_id",
          "position",
          "alt",
          "caption"
        ]
      },
      "courses": {
        "type": "object",
        "description": "Courses Alex is taking at Centennial College",
        "properties": {
          "code": {
            "type": "string",
            "description": "Course code, for example COMP 307"
          },
          "name": {
            "type": "string",
            "description": "Course name"
          },
          "term": {
            "type": "string",
            "description": "Term, for example Fall 2026"
          },
          "topics": {
            "type": [
              "string",
              "null"
            ],
            "description": "Topics covered, NULL until filled in"
          }
        },
        "required": [
          "code",
          "name",
          "term",
          "topics"
        ]
      },
      "timeline": {
        "type": "object",
        "description": "Work, education and project events by date",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Position in date order, 1 first"
          },
          "date": {
            "type": "string",
            "description": "YYYY-MM, or YYYY when the month is not known"
          },
          "kind": {
            "type": "string",
            "enum": [
              "work",
              "education",
              "project",
              "life"
            ],
            "description": "work, education, project or life"
          },
          "event": {
            "type": "string",
            "description": "What happened"
          }
        },
        "required": [
          "id",
          "date",
          "kind",
          "event"
        ]
      },
      "pets": {
        "type": "object",
        "description": "The cats Alex lives with",
        "properties": {
          "name": {
            "type": "string",
            "description": "The cat"
          },
          "species": {
            "type": "string",
            "description": "Always cat so far"
          },
          "breed": {
            "type": "string",
            "description": "Breed"
          },
          "born": {
            "type": "integer",
            "description": "Birth year; age is derived from it"
          },
          "personality": {
            "type": "string",
            "description": "Alex's description"
          },
          "photo_url": {
            "type": "string",
            "description": "Path of a 480 by 480 photo on this site"
          },
          "photo_alt": {
            "type": "string",
            "description": "Alt text for that photo"
          }
        },
        "required": [
          "name",
          "species",
          "breed",
          "born",
          "personality",
          "photo_url",
          "photo_alt"
        ]
      },
      "experience": {
        "type": "object",
        "description": "Jobs and roles Alex has held",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Position in start-date order, 1 first"
          },
          "title": {
            "type": "string",
            "description": "Job title"
          },
          "organization": {
            "type": "string",
            "description": "Who Alex worked for"
          },
          "location": {
            "type": "string",
            "description": "Where the job was"
          },
          "start": {
            "type": "string",
            "description": "YYYY-MM, or YYYY when the month is not known"
          },
          "end": {
            "type": [
              "string",
              "null"
            ],
            "description": "The same form as start, NULL if current"
          },
          "summary": {
            "type": "string",
            "description": "One line on the job, as on the plain-text resume"
          },
          "highlights": {
            "type": "string",
            "description": "What Alex did there, word for word from his resume, one per line"
          }
        },
        "required": [
          "id",
          "title",
          "organization",
          "location",
          "start",
          "end",
          "summary",
          "highlights"
        ]
      },
      "interests": {
        "type": "object",
        "description": "Alex's hobbies and favourites, in his words",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Position in the list, 1 first"
          },
          "category": {
            "type": "string",
            "enum": [
              "video game",
              "game genre",
              "playing now",
              "board game",
              "movie",
              "TV show",
              "watching now",
              "history topic",
              "reading now",
              "music genre",
              "music artist",
              "YouTube or podcast",
              "sport",
              "following",
              "travelled to",
              "wants to visit"
            ],
            "description": "video game, game genre, playing now, board game, movie, TV show, watching now, history topic, reading now, music genre, music artist, YouTube or podcast, sport, following, travelled to, or wants to visit"
          },
          "name": {
            "type": "string",
            "description": "The game, show, team, place or other thing"
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alex's note on it, NULL if none"
          }
        },
        "required": [
          "id",
          "category",
          "name",
          "note"
        ]
      },
      "storage": {
        "type": "object",
        "description": "What this site stores for a visitor or a question, how long, and why, from the constants the code uses",
        "properties": {
          "item": {
            "type": "string",
            "description": "What is stored"
          },
          "kept_for": {
            "type": "string",
            "description": "How long it is kept"
          },
          "purpose": {
            "type": "string",
            "description": "Why"
          }
        },
        "required": [
          "item",
          "kept_for",
          "purpose"
        ]
      },
      "uses": {
        "type": "object",
        "description": "The machines, peripherals and software Alex uses, and what he is learning, as listed on /uses",
        "properties": {
          "position": {
            "type": "integer",
            "description": "Order on the page, 1 first"
          },
          "section": {
            "type": "string",
            "enum": [
              "Machines",
              "Peripherals",
              "Software",
              "Learning"
            ],
            "description": "Machines, Peripherals, Software or Learning"
          },
          "item": {
            "type": "string",
            "description": "What it is, for example Main PC"
          },
          "details": {
            "type": "string",
            "description": "What Alex uses"
          }
        },
        "required": [
          "position",
          "section",
          "item",
          "details"
        ]
      },
      "sections": {
        "type": "object",
        "description": "The text of the site's pages and case studies, one row per section",
        "properties": {
          "page": {
            "type": "string",
            "description": "Where the text appears: /#about or /#now on the home page, /404, or /work/ followed by a project's slug, such as /work/uraz-hoops"
          },
          "position": {
            "type": "integer",
            "description": "Order on the page, 1 first"
          },
          "heading": {
            "type": "string",
            "description": "The section's heading as the page shows it"
          },
          "body": {
            "type": "string",
            "description": "The section's text as the page shows it; a blank line between paragraphs, list items starting with \"- \""
          }
        },
        "required": [
          "page",
          "position",
          "heading",
          "body"
        ]
      },
      "page_images": {
        "type": "object",
        "description": "The photos on the site's other pages",
        "properties": {
          "page": {
            "type": "string",
            "description": "Where the photo appears, as in sections.page"
          },
          "position": {
            "type": "integer",
            "description": "Order on the page, 1 first"
          },
          "alt": {
            "type": "string",
            "description": "What the photo shows"
          },
          "caption": {
            "type": [
              "string",
              "null"
            ],
            "description": "Caption under the photo, NULL if none"
          }
        },
        "required": [
          "page",
          "position",
          "alt",
          "caption"
        ]
      },
      "project_detail": {
        "description": "A projects row with the names of its technologies",
        "allOf": [
          {
            "$ref": "#/components/schemas/projects"
          },
          {
            "type": "object",
            "properties": {
              "technologies": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Names from the technologies table"
              }
            },
            "required": [
              "technologies"
            ]
          }
        ]
      },
      "database_schema": {
        "type": "object",
        "description": "The tables and columns of the database, the DDL that created it and a hash of both",
        "properties": {
          "hash": {
            "type": "string",
            "description": "SHA-256 of the DDL, the table list, the fact keys with their descriptions and the section headings"
          },
          "ddl": {
            "type": "string",
            "description": "The CREATE TABLE statements, with a comment per column"
          },
          "tables": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "primaryKey": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "columns": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string",
                        "enum": [
                          "INTEGER",
                          "TEXT"
                        ]
                      },
                      "nullable": {
                        "type": "boolean"
                      },
                      "unique": {
                        "type": "boolean"
                      },
                      "values": {
                        "type": "array",
                        "items": {
                          "type": [
                            "string",
                            "integer"
                          ]
                        },
                        "description": "The allowed values, when the column has a CHECK"
                      },
                      "description": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "name",
                      "type",
                      "nullable",
                      "unique",
                      "description"
                    ]
                  }
                }
              },
              "required": [
                "name",
                "description",
                "primaryKey",
                "columns"
              ]
            }
          },
          "factKeys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The keys of the facts table"
          },
          "sectionPages": {
            "type": "array",
            "description": "The pages the sections table holds, each once, sorted",
            "items": {
              "type": "string"
            }
          },
          "sectionHeadings": {
            "type": "array",
            "description": "The headings the sections table holds, in page order",
            "items": {
              "type": "string"
            }
          },
          "facts": {
            "type": "array",
            "description": "The keys of the facts table, each with what it means",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              },
              "required": [
                "key",
                "description"
              ]
            }
          },
          "photoAlt": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Alt text by photo path, for the pets photos"
          }
        },
        "required": [
          "hash",
          "ddl",
          "tables",
          "factKeys",
          "facts",
          "sectionPages",
          "sectionHeadings",
          "photoAlt"
        ]
      },
      "ask_request": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string",
            "minLength": 3,
            "maxLength": 200,
            "description": "A question about the data, in plain words, 3 to 200 characters after trimming"
          }
        },
        "required": [
          "question"
        ]
      },
      "ask_answer": {
        "type": "object",
        "properties": {
          "sql": {
            "type": "string",
            "description": "A single SELECT or WITH statement prepared against the database, or empty when the question cannot be answered from it"
          },
          "explanation": {
            "type": "string",
            "description": "One sentence on what the SQL does, or on why there is none"
          },
          "cached": {
            "type": "boolean",
            "description": "True when the answer came from the cache rather than the model"
          },
          "token": {
            "type": "string",
            "description": "Lets the visitor send this question to Alex through /api/questions within 10 minutes"
          }
        },
        "required": [
          "sql",
          "explanation",
          "cached",
          "token"
        ]
      },
      "send_request": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string",
            "minLength": 3,
            "maxLength": 200,
            "description": "The question exactly as it was asked, with no control or text-direction characters"
          },
          "token": {
            "type": "string",
            "description": "The token /api/ask returned with that question"
          }
        },
        "required": [
          "question",
          "token"
        ]
      },
      "send_answer": {
        "type": "object",
        "properties": {
          "sent": {
            "const": true,
            "description": "The same whether the question is new or was sent before"
          }
        },
        "required": [
          "sent"
        ]
      },
      "unavailable": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "budget",
              "config",
              "upstream"
            ],
            "description": "budget when the monthly cap is reached, config when the service is not set up, upstream when the model or the store did not answer"
          }
        },
        "required": [
          "reason"
        ]
      },
      "stats": {
        "type": "object",
        "properties": {
          "questionsThisMonth": {
            "type": "integer",
            "description": "Questions answered this UTC calendar month, cached answers and refusals included"
          },
          "modelCallsThisMonth": {
            "type": "integer",
            "description": "Model calls attempted this month, retries and failures included; this is what the cap counts"
          },
          "cap": {
            "type": "integer",
            "description": "Model calls allowed per month; 0 means asking is switched off"
          },
          "model": {
            "type": "string",
            "description": "The model id in use"
          },
          "commit": {
            "type": "string",
            "description": "The commit the site was built from"
          },
          "builtAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the site was built"
          }
        },
        "required": [
          "questionsThisMonth",
          "modelCallsThisMonth",
          "cap",
          "model",
          "commit",
          "builtAt"
        ]
      }
    }
  }
}
