{"openapi":"3.1.0","info":{"title":"Desync Observatory API","description":"Hybrid search + knowledge graph over a Catholic-tech corpus.","version":"0.4.0"},"paths":{"/health":{"get":{"summary":"Health","description":"Returns app + dependency liveness. Doesn't actually ping the DB on every\ncall (would explode under load); instead reports the lifespan state.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Health Get"}}}}}}},"/health/worker":{"get":{"summary":"Worker Health","description":"Read the worker heartbeat so a dead worker stops looking like a healthy one.\n\n#1364 added the heartbeat write; nothing ever read it, so worker liveness stayed\nexactly as unobservable as before (#1408). This is the read half.\n\n``status`` is deliberately four-valued rather than a boolean, because \"no heartbeat\"\nand \"stale heartbeat\" are different facts with different responses:\n\n- ``never``  — the table is empty: no worker has ever beaten against this database.\n  That is the *current, expected* state (the heartbeat lives on ``dev`` while the\n  live worker still runs from ``main``), so it must not be reported as a failure.\n- ``paused`` — beating, with ``AGG_WORKER_AUTOSTART=false``: alive and deliberately\n  not processing. Distinguishing this from ``ok`` is the whole point of the\n  ``paused`` column — otherwise a deliberately-idle worker hides a stalled pipeline.\n- ``ok``     — beat within ``STALE_AFTER_SECONDS``.\n- ``stale``  — a heartbeat exists but has stopped advancing. This is the alarm.\n\nTolerates the table being absent (fresh database, test container that hasn't run\n``sql/017``) rather than 500-ing, mirroring how ``_init_age_session`` tolerates AGE\nnot being installed — a liveness probe must not itself become a source of outages.","operationId":"worker_health_health_worker_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Worker Health Health Worker Get"}}}}}}},"/api/articles/{article_id}":{"get":{"summary":"Get Article","operationId":"get_article_api_articles__article_id__get","parameters":[{"name":"article_id","in":"path","required":true,"schema":{"type":"integer","title":"Article Id"}},{"name":"original","in":"query","required":false,"schema":{"type":"boolean","description":"return original-language text instead of English","default":false,"title":"Original"},"description":"return original-language text instead of English"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tags":{"get":{"summary":"List Tags","operationId":"list_tags_api_tags_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/sources":{"get":{"summary":"List Sources","operationId":"list_sources_api_sources_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"source_type","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Source Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourcesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/stats":{"get":{"summary":"Stats","operationId":"stats_api_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}}}}},"/api/analytics":{"get":{"summary":"Analytics","description":"Serve the **precomputed** snapshot. This path must never aggregate the corpus.\n\nIt used to. There was an in-process TTL cache, which sounds sufficient and is not: it\nis per-process, so it is empty after every deploy and restart, and it expires on a wall\nclock rather than on new data. Whoever arrived at that moment paid for 17 full-corpus\naggregates on their own page load — measured 2026-08-15 at **4.46 s cold vs 0.19 s\nwarm**, and earlier the same day a cold recompute blew the 15 s statement timeout,\nreturned 500, and the page rendered its error state *with a 200 status*. Cloudflare's\n`edge_ttl 300s, override_origin` rule then served that error from the edge for five\nminutes, to everyone, with nothing retrying it. Maks hit exactly that.\n\nSo the computation moved to the worker (`refresh_analytics_snapshot`), which is where\nslow periodic work belongs. This endpoint is now one indexed SELECT of a prepared JSON\ndocument, and a visitor can no longer be billed for the archive's size.\n\n**The fallback still computes**, but only when the snapshot row does not exist at all —\na fresh database, or before the worker's first tick. That path keeps the endpoint\nworking on day one rather than serving an empty page while the worker warms up.","operationId":"analytics_api_analytics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsResponse"}}}}}}},"/api/sitemap":{"get":{"summary":"Sitemap","description":"Ids and dates for `sitemap.xml`.\n\n**Why `window` exists (#1543).** A sitemap must list what an anonymous visitor can actually\nread; anything else queues crawlers onto pages that turn them away. #1542 capped the sitemap\nat the window's *size*, which is necessary and not sufficient — the two queries still answered\nthe same question with different sort keys (`published_at` vs `fetched_at`) and a\n*separately hardcoded* relevance filter.\n\nMeasured on production 2026-08-19 at limit=100, as `CATHOLIC_OS_ANON_WINDOW_MIN_RELEVANCE`\nmoves while this endpoint's hardcoded `0.5` does not:\n\n    0.5 (today)   98/100 overlap      2 sitemap URLs answer \"Sign in to read\"\n    0.4           85/100             15 paywalled\n    0.3           56/100             44 paywalled\n    0.1           32/100             68 paywalled\n    unset          0/100            100 paywalled\n\nThe worst row is the **fail-open default**: unset means \"gate off\" to the window and still\nmeans `>= 0.5` here, so the safest-looking configuration is the one that paywalls the entire\nsitemap. A coupling that fails hardest at its own default is not a coupling anyone will\nnotice — hence one query, shared, rather than two that agree by coincidence.","operationId":"sitemap_api_sitemap_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20000,"minimum":1,"default":5000,"title":"Limit"}},{"name":"window","in":"query","required":false,"schema":{"type":"boolean","description":"Return exactly the anonymous reading window's rows, from the window's own query. The caller passes this when the gate is on, together with the same min_relevance the gate uses.","default":false,"title":"Window"},"description":"Return exactly the anonymous reading window's rows, from the window's own query. The caller passes this when the gate is on, together with the same min_relevance the gate uses."},{"name":"min_relevance","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Min Relevance"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SitemapResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/quota/article-view":{"post":{"summary":"Consume Article View","operationId":"consume_article_view_api_quota_article_view_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/quota/status":{"get":{"summary":"Quota Status","description":"Read a reader's allowance **without spending any of it**.\n\nDeliberately separate from `POST /api/quota/article-view`. Reusing the consume path to\nrender a header would have charged a view for looking at the header — the reader would\nwatch their own quota drain by navigating, which is the sort of bug that is obvious\nafterwards and invisible in a unit test that only checks the returned number.\n\n`resets_at` is sent as an absolute instant rather than \"in 6 hours\", because the row is\nkeyed on `current_date` in the database's timezone and only the server knows when that\nflips. Formatting it for the reader is the frontend's business.","operationId":"quota_status_api_quota_status_get","parameters":[{"name":"user_id","in":"query","required":true,"schema":{"type":"string","title":"User Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":1000000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reader/window":{"get":{"summary":"Reader Window","description":"The window, as an explicit choice between two readings of the same rule.\n\nMaks's rule is \"the last N articles we pulled\". Taken literally — `min_relevance`\nunset — N=100 reaches back **four minutes**, because we pull 350–794 articles a day\nand roughly 80% of them score 0.0: the classifier's job is to find tech-Catholic\nmaterial inside a general Catholic corpus, so most of what we pull is deliberately\noff-topic for this site.\n\nSetting `min_relevance` counts the window over articles the site actually ranks up\ninstead, which is what a visitor perceives as \"the last 100 articles\" — they never see\nthe rest. At `0.5` that reaches back about three days.\n\n**The tradeoff, stated so it is not discovered later.** A relevance-counted window\ndepends on classification keeping up. If the classifier stalls — it was down for three\nweeks in July 2026 — nothing new gets scored, and the window silently stops advancing\nwhile every other signal stays green. The literal reading has no such coupling. Pick\nwith that in mind; `docs/ROADMAP.md` §H2 carries the costed options.","operationId":"reader_window_api_reader_window_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":100,"title":"Limit"}},{"name":"min_relevance","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"description":"Count the window over articles scoring at least this, instead of over everything we pulled. Unset (the default) means count everything.","title":"Min Relevance"},"description":"Count the window over articles scoring at least this, instead of over everything we pulled. Unset (the default) means count everything."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReaderWindow"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health/reader-window":{"get":{"summary":"Reader Window Health","description":"Measure the window an anonymous visitor gets, and whether it covers the front page.\n\n**The parameters are the caller's, not the deployment's.** The gate is configured on the\n*frontend* service (`CATHOLIC_OS_ANON_ARTICLE_LIMIT`,\n`CATHOLIC_OS_ANON_WINDOW_MIN_RELEVANCE`); the API cannot read another service's\nenvironment, so it cannot know them and does not pretend to. The defaults are what\nproduction is set to, and a caller measuring something else gets what they asked for.\nSaying so beats inventing a number that would silently describe the wrong window.","operationId":"reader_window_health_health_reader_window_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":100,"title":"Limit"}},{"name":"min_relevance","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"description":"The window being measured. Defaults to production's configuration (limit=100, min_relevance=0.5). Pass min_relevance= (empty) to measure the literal 'last N we pulled' reading instead.","default":0.5,"title":"Min Relevance"},"description":"The window being measured. Defaults to production's configuration (limit=100, min_relevance=0.5). Pass min_relevance= (empty) to measure the literal 'last N we pulled' reading instead."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Reader Window Health Health Reader Window Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/health/sources":{"get":{"summary":"Barren Sources","operationId":"barren_sources_health_sources_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Barren Sources Health Sources Get"}}}}}}},"/health/ingest":{"get":{"summary":"Ingest Health","operationId":"ingest_health_health_ingest_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ingest Health Health Ingest Get"}}}}}}},"/health/stages":{"get":{"summary":"Stage Health","operationId":"stage_health_health_stages_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Stage Health Health Stages Get"}}}}}}},"/health/transcription":{"get":{"summary":"Transcription Health","operationId":"transcription_health_health_transcription_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Transcription Health Health Transcription Get"}}}}}}},"/api/search":{"get":{"summary":"Hybrid Search","operationId":"hybrid_search_api_search_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"tags","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Tags"}},{"name":"source_type","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Source Type"}},{"name":"from_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"From Date"}},{"name":"to_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"To Date"}},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Language"}},{"name":"content_type","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Content Type"}},{"name":"min_relevance","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Min Relevance"}},{"name":"alpha","in":"query","required":false,"schema":{"type":"number","maximum":1.0,"minimum":0.0,"default":0.5,"title":"Alpha"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"original","in":"query","required":false,"schema":{"type":"boolean","description":"return original-language text instead of English","default":false,"title":"Original"},"description":"return original-language text instead of English"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/entities":{"get":{"summary":"List Entities","description":"List canonical entities (excludes merged duplicates via ``canonical_id IS NULL``).\n\nThe ``total`` field applies the same filter set as the page query, so\npaginated UIs compute correct page counts (mirrors the /api/sources fix\nin 4C).","operationId":"list_entities_api_entities_get","parameters":[{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Type"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"min_articles","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":1,"title":"Min Articles"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitiesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/graph":{"get":{"summary":"Graph","operationId":"graph_api_graph_get","parameters":[{"name":"center_id","in":"query","required":true,"schema":{"type":"string","description":"article:N or entity:N","title":"Center Id"},"description":"article:N or entity:N"},{"name":"depth","in":"query","required":false,"schema":{"type":"integer","maximum":3,"minimum":1,"default":1,"title":"Depth"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"include_relations","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Relations"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/graph/path":{"get":{"summary":"Graph Path","description":"Shortest path between two entities via recursive CTE on cooccurrences.\n\nReturns a ``GraphResponse`` whose nodes are the entities along the path\nand whose edges are the co_occurs edges connecting them. If no path within\n``max_hops`` exists, returns both endpoint nodes + zero edges (200 OK)\nso the frontend can render a \"no connection found\" message anchored on\nboth nodes.\n\nImplementation: a recursive CTE accumulates the path as an ARRAY column,\npruning extensions that would revisit a node already in the path. We\nORDER BY depth and LIMIT 1 to take the shortest path that reaches the\ndestination. UNION ALL is used (not UNION) because we want to enumerate\ndistinct paths, not just visited node sets — the cycle prevention is the\nexplicit ``<> ALL(p.path)`` clause.\n\nPerformance: bounded by ``statement_timeout``. On dense graphs paths-\nenumeration can blow up; the cap of MAX_PATH_HOPS=4 plus the 8s timeout\ngives us a 504 with a friendly message rather than a hung worker if a\nquery gets pathological.","operationId":"graph_path_api_graph_path_get","parameters":[{"name":"from_entity_id","in":"query","required":true,"schema":{"type":"integer","minimum":1,"title":"From Entity Id"}},{"name":"to_entity_id","in":"query","required":true,"schema":{"type":"integer","minimum":1,"title":"To Entity Id"}},{"name":"max_hops","in":"query","required":false,"schema":{"type":"integer","maximum":4,"minimum":1,"default":4,"title":"Max Hops"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/graph/transitive":{"get":{"summary":"Graph Transitive","description":"K-hop fan-out from a center entity via recursive CTE.\n\nReturns up to ``limit`` distinct entities reachable within ``depth`` hops\nalong ``entity_cooccurrences`` edges, plus the edges that connect the\nreturned subgraph (so the frontend can render edges between expanded\nnodes, not just spokes from the center).\n\nThe recursive CTE uses ``UNION`` (not ``UNION ALL``) for visited-set\nsemantics: each entity is visited at most once, capped at the smallest\nhop count it was reached at. This is the key reason this endpoint is\nfast on the dense Catholic OS corpus where AGE's walk-enumerating\nCypher pattern blows up (Phase 7A benchmark: AGE 30s timeout at depth 2\nvs SQL CTE 200ms).\n\nOptional ``include_relations=true`` overlays typed `entity_relations`\nedges where both endpoints are in the visited set.","operationId":"graph_transitive_api_graph_transitive_get","parameters":[{"name":"center_id","in":"query","required":true,"schema":{"type":"string","description":"entity:N","title":"Center Id"},"description":"entity:N"},{"name":"depth","in":"query","required":false,"schema":{"type":"integer","maximum":3,"minimum":1,"default":2,"title":"Depth"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":120,"title":"Limit"}},{"name":"include_relations","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Relations"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/translate":{"get":{"summary":"Translate Article","operationId":"translate_article_api_translate_get","parameters":[{"name":"article_id","in":"query","required":true,"schema":{"type":"integer","minimum":1,"title":"Article Id"}},{"name":"lang","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":5,"title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/suggestions":{"post":{"summary":"Create Suggestion","operationId":"create_suggestion_suggestions_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestionIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Create Suggestion Suggestions Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reports":{"post":{"summary":"Create Report","operationId":"create_report_reports_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Create Report Reports Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/notify":{"post":{"summary":"Create Notify Signup","operationId":"create_notify_signup_api_notify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifyIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Create Notify Signup Api Notify Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AnalyticsResponse":{"properties":{"generated_at":{"type":"string","format":"date-time","title":"Generated At"},"corpus":{"$ref":"#/components/schemas/CorpusTotals"},"tag_discovery":{"items":{"$ref":"#/components/schemas/MonthPoint"},"type":"array","title":"Tag Discovery"},"tag_assignments":{"items":{"$ref":"#/components/schemas/MonthPoint"},"type":"array","title":"Tag Assignments"},"top_topics_by_month":{"items":{"$ref":"#/components/schemas/MonthTop"},"type":"array","title":"Top Topics By Month"},"top_entities_by_month":{"items":{"$ref":"#/components/schemas/MonthTop"},"type":"array","title":"Top Entities By Month"},"fastest_growing_entities":{"items":{"$ref":"#/components/schemas/EntityGrowth"},"type":"array","title":"Fastest Growing Entities"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats"}},"type":"object","required":["generated_at","corpus","tag_discovery","tag_assignments","top_topics_by_month","top_entities_by_month","fastest_growing_entities","caveats"],"title":"AnalyticsResponse"},"ArticleDetail":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"relevance_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Relevance Score"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"source_id":{"type":"integer","title":"Source Id"},"source_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Name"},"content_type":{"type":"string","enum":["article","video","podcast_episode","audio","paper"],"title":"Content Type"},"external_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Url"},"is_translated":{"type":"boolean","title":"Is Translated","default":false},"content_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Text"},"transcript":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Transcript"},"media_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Media Url"},"transcript_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Transcript Summary"},"transcript_topics":{"items":{"type":"string"},"type":"array","title":"Transcript Topics","default":[]},"transcript_summary_excerpted":{"type":"boolean","title":"Transcript Summary Excerpted","default":false},"yt_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Yt Description"},"embedded_video_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Embedded Video Url"},"original_links":{"items":{"additionalProperties":{"type":"string"},"type":"object"},"type":"array","title":"Original Links","default":[]},"content_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Html"},"content_clean_html":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content Clean Html"},"body_format":{"type":"string","title":"Body Format","default":"plain"},"hidden_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hidden Reason"},"lead_image_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lead Image Url"},"authors":{"items":{"type":"string"},"type":"array","title":"Authors"},"images":{"items":{"$ref":"#/components/schemas/ArticleImageRef"},"type":"array","title":"Images"},"fetched_at":{"type":"string","format":"date-time","title":"Fetched At"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata"},"entities":{"items":{"$ref":"#/components/schemas/EntityRef"},"type":"array","title":"Entities"},"tags":{"items":{"$ref":"#/components/schemas/TagRef"},"type":"array","title":"Tags"},"related_articles":{"items":{"$ref":"#/components/schemas/ArticleSummary"},"type":"array","title":"Related Articles"},"sections":{"items":{"$ref":"#/components/schemas/ArticleSectionRef"},"type":"array","title":"Sections"}},"type":"object","required":["id","source_id","content_type","fetched_at","entities","tags","related_articles"],"title":"ArticleDetail"},"ArticleImageRef":{"properties":{"ordinal":{"type":"integer","title":"Ordinal"},"src_url":{"type":"string","title":"Src Url"},"alt_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Alt Text"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption"},"width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Width"},"height":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Height"},"classification":{"type":"string","title":"Classification","default":"figure"}},"type":"object","required":["ordinal","src_url"],"title":"ArticleImageRef"},"ArticleSectionRef":{"properties":{"ordinal":{"type":"integer","title":"Ordinal"},"title":{"type":"string","title":"Title"},"anchor":{"type":"string","title":"Anchor"}},"type":"object","required":["ordinal","title","anchor"],"title":"ArticleSectionRef","description":"One section of a body, located by quotation rather than by offset.\n\n`anchor` is the section's opening words exactly as they appear in the stored\nbody. The frontend renders a reflowed body, so a character offset into the\nstored text addresses nothing in the rendered HTML — words survive that\ntransform, offsets do not."},"ArticleSummary":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"relevance_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Relevance Score"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"source_id":{"type":"integer","title":"Source Id"},"source_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Name"},"content_type":{"type":"string","enum":["article","video","podcast_episode","audio","paper"],"title":"Content Type"},"external_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Url"},"is_translated":{"type":"boolean","title":"Is Translated","default":false}},"type":"object","required":["id","source_id","content_type"],"title":"ArticleSummary"},"CorpusTotals":{"properties":{"total_articles":{"type":"integer","title":"Total Articles"},"hours_with_transcript":{"type":"number","title":"Hours With Transcript"},"hours_machine_transcribed":{"type":"number","title":"Hours Machine Transcribed"},"hours_awaiting_transcript":{"type":"number","title":"Hours Awaiting Transcript"},"characters_of_text":{"type":"integer","title":"Characters Of Text"},"characters_of_transcript":{"type":"integer","title":"Characters Of Transcript"},"characters_translated":{"type":"integer","title":"Characters Translated"},"articles_translated":{"type":"integer","title":"Articles Translated"}},"type":"object","required":["total_articles","hours_with_transcript","hours_machine_transcribed","hours_awaiting_transcript","characters_of_text","characters_of_transcript","characters_translated","articles_translated"],"title":"CorpusTotals","description":"Corpus-wide totals for the analytics page.\n\nThe transcript hours are split three ways deliberately. Most of the material that\n*has* a transcript came from publisher captions we downloaded rather than from our\nown transcription, so a single \"hours transcribed\" number would overstate the work\nby roughly 3x. `hours_awaiting_transcript` is video we hold with a duration and no\ntranscript at all."},"EntitiesResponse":{"properties":{"entities":{"items":{"$ref":"#/components/schemas/EntitySummary"},"type":"array","title":"Entities"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["entities","total","limit","offset"],"title":"EntitiesResponse"},"EntityGrowth":{"properties":{"name":{"type":"string","title":"Name"},"previous":{"type":"integer","title":"Previous"},"current":{"type":"integer","title":"Current"},"delta":{"type":"integer","title":"Delta"},"previous_month":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Month"},"current_month":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Month"}},"type":"object","required":["name","previous","current","delta"],"title":"EntityGrowth","description":"An entity's movement between the two most recent months.\n\n`previous`/`current` are article counts, not mention counts — we store one row per\n(article, entity) pair, so true in-text occurrence counts are data we do not collect."},"EntityRef":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"entity_type":{"type":"string","enum":["person","organization","project","document","event","location","concept","university","journal"],"title":"Entity Type"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["id","name","entity_type"],"title":"EntityRef"},"EntitySummary":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"entity_type":{"type":"string","enum":["person","organization","project","document","event","location","concept","university","journal"],"title":"Entity Type"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"article_count":{"type":"integer","title":"Article Count"}},"type":"object","required":["id","name","entity_type","article_count"],"title":"EntitySummary"},"GraphEdge":{"properties":{"source":{"type":"string","title":"Source"},"target":{"type":"string","title":"Target"},"type":{"type":"string","enum":["mentions","co_occurs","relates_to"],"title":"Type"},"weight":{"type":"number","title":"Weight"},"predicate":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Predicate"}},"type":"object","required":["source","target","type","weight"],"title":"GraphEdge"},"GraphNode":{"properties":{"id":{"type":"string","title":"Id"},"type":{"type":"string","enum":["article","entity"],"title":"Type"},"label":{"type":"string","title":"Label"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata"}},"type":"object","required":["id","type","label"],"title":"GraphNode"},"GraphResponse":{"properties":{"center":{"type":"string","title":"Center"},"depth":{"type":"integer","title":"Depth"},"nodes":{"items":{"$ref":"#/components/schemas/GraphNode"},"type":"array","title":"Nodes"},"edges":{"items":{"$ref":"#/components/schemas/GraphEdge"},"type":"array","title":"Edges"}},"type":"object","required":["center","depth","nodes","edges"],"title":"GraphResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"MonthPoint":{"properties":{"month":{"type":"string","title":"Month"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["month","count"],"title":"MonthPoint","description":"One point on a monthly series. `month` is 'YYYY-MM'."},"MonthTop":{"properties":{"month":{"type":"string","title":"Month"},"items":{"items":{"$ref":"#/components/schemas/TopItem"},"type":"array","title":"Items"}},"type":"object","required":["month","items"],"title":"MonthTop","description":"A month's top-N ranking."},"NotifyIn":{"properties":{"email":{"type":"string","title":"Email"},"source_page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Page"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"}},"type":"object","required":["email"],"title":"NotifyIn"},"QuotaRequest":{"properties":{"user_id":{"type":"string","maxLength":255,"minLength":1,"title":"User Id"},"article_id":{"type":"integer","title":"Article Id"},"limit":{"type":"integer","maximum":1000000.0,"exclusiveMinimum":0.0,"title":"Limit"}},"type":"object","required":["user_id","article_id","limit"],"title":"QuotaRequest"},"QuotaResponse":{"properties":{"allowed":{"type":"boolean","title":"Allowed"},"used":{"type":"integer","title":"Used"},"limit":{"type":"integer","title":"Limit"}},"type":"object","required":["allowed","used","limit"],"title":"QuotaResponse"},"QuotaStatus":{"properties":{"used":{"type":"integer","title":"Used"},"limit":{"type":"integer","title":"Limit"},"remaining":{"type":"integer","title":"Remaining"},"resets_at":{"type":"string","title":"Resets At"}},"type":"object","required":["used","limit","remaining","resets_at"],"title":"QuotaStatus","description":"What a signed-in reader is shown about their own allowance."},"ReaderWindow":{"properties":{"article_ids":{"items":{"type":"integer"},"type":"array","title":"Article Ids"}},"type":"object","required":["article_ids"],"title":"ReaderWindow","description":"The ids an anonymous visitor may open, newest pull first."},"ReportIn":{"properties":{"article_id":{"type":"integer","title":"Article Id"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"}},"type":"object","required":["article_id"],"title":"ReportIn"},"SearchHit":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"relevance_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Relevance Score"},"published_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Published At"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"source_id":{"type":"integer","title":"Source Id"},"source_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Name"},"content_type":{"type":"string","enum":["article","video","podcast_episode","audio","paper"],"title":"Content Type"},"external_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Url"},"is_translated":{"type":"boolean","title":"Is Translated","default":false},"rrf_score":{"type":"number","title":"Rrf Score"},"match_score":{"type":"number","title":"Match Score","default":0.0},"matched_chunk_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matched Chunk Text"},"bm25_rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bm25 Rank"},"cosine_rank":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cosine Rank"}},"type":"object","required":["id","source_id","content_type","rrf_score"],"title":"SearchHit"},"SearchResponse":{"properties":{"query":{"type":"string","title":"Query"},"total_estimated":{"type":"integer","title":"Total Estimated"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"hits":{"items":{"$ref":"#/components/schemas/SearchHit"},"type":"array","title":"Hits"}},"type":"object","required":["query","total_estimated","limit","offset","hits"],"title":"SearchResponse"},"SitemapEntry":{"properties":{"id":{"type":"integer","title":"Id"},"last_modified":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Modified"}},"type":"object","required":["id"],"title":"SitemapEntry","description":"One indexable article: just enough for a sitemap `<url>` entry."},"SitemapResponse":{"properties":{"articles":{"items":{"$ref":"#/components/schemas/SitemapEntry"},"type":"array","title":"Articles"}},"type":"object","required":["articles"],"title":"SitemapResponse"},"SourceWithStats":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"url":{"type":"string","title":"Url"},"source_type":{"type":"string","enum":["rss","web","youtube","podcast","arxiv"],"title":"Source Type"},"is_active":{"type":"boolean","title":"Is Active"},"article_count":{"type":"integer","title":"Article Count"},"last_fetched_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Fetched At"},"consecutive_failures":{"type":"integer","title":"Consecutive Failures"}},"type":"object","required":["id","name","url","source_type","is_active","article_count","consecutive_failures"],"title":"SourceWithStats"},"SourcesResponse":{"properties":{"sources":{"items":{"$ref":"#/components/schemas/SourceWithStats"},"type":"array","title":"Sources"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["sources","total"],"title":"SourcesResponse"},"StatsResponse":{"properties":{"total_articles":{"type":"integer","title":"Total Articles"},"total_classified":{"type":"integer","title":"Total Classified"},"total_relevant":{"type":"integer","title":"Total Relevant"},"total_chunks":{"type":"integer","title":"Total Chunks"},"canonical_entities":{"type":"integer","title":"Canonical Entities"},"resolved_duplicates":{"type":"integer","title":"Resolved Duplicates"},"controlled_tags":{"type":"integer","title":"Controlled Tags"},"sources_active":{"type":"integer","title":"Sources Active"},"sources_failing":{"type":"integer","title":"Sources Failing"},"sources_stalled":{"type":"integer","title":"Sources Stalled","default":0},"newest_relevant_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Newest Relevant At"},"languages":{"items":{"type":"string"},"type":"array","title":"Languages"},"last_pipeline_run":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Pipeline Run"}},"type":"object","required":["total_articles","total_classified","total_relevant","total_chunks","canonical_entities","resolved_duplicates","controlled_tags","sources_active","sources_failing","languages"],"title":"StatsResponse"},"SuggestionIn":{"properties":{"url":{"type":"string","title":"Url"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"}},"type":"object","required":["url"],"title":"SuggestionIn"},"TagRef":{"properties":{"id":{"type":"integer","title":"Id"},"slug":{"type":"string","title":"Slug"},"name":{"type":"string","title":"Name"},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"}},"type":"object","required":["id","slug","name"],"title":"TagRef"},"TagWithCount":{"properties":{"id":{"type":"integer","title":"Id"},"slug":{"type":"string","title":"Slug"},"name":{"type":"string","title":"Name"},"article_count":{"type":"integer","title":"Article Count"}},"type":"object","required":["id","slug","name","article_count"],"title":"TagWithCount"},"TagsResponse":{"properties":{"tags":{"items":{"$ref":"#/components/schemas/TagWithCount"},"type":"array","title":"Tags"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["tags","total"],"title":"TagsResponse"},"TopItem":{"properties":{"name":{"type":"string","title":"Name"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["name","count"],"title":"TopItem"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}