{"openapi":"3.1.0","info":{"title":"Norma API","version":"0.1.0","summary":"API RAG per domande normative dei docenti.","description":"Integrazione FE↔BE stateless: il client ottiene un access token HMAC (JWT-like) da `POST /api/session` e lo invia come `Authorization: Bearer <token>` sulle route protette. Retrieval multi-aspetto, sintesi in italiano, score di affidabilità. Nessuna sessione server-side."},"servers":[{"url":"/","description":"Stessa origine dell’app"}],"tags":[{"name":"Auth","description":"Emissione token di accesso (stateless)."},{"name":"RAG","description":"Domande, sintesi in italiano e claim citati."},{"name":"LLM","description":"Riformulazione opzionale vincolata alle citazioni."},{"name":"Ops","description":"Cron di controllo della normativa, senza ingestione."},{"name":"Meta","description":"Contratto e stato."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Access token firmato HS256 emesso da `/api/session`."}},"schemas":{"SessionResponse":{"type":"object","required":["accessToken","tokenType","expiresIn","expiresAt"],"properties":{"accessToken":{"type":"string"},"tokenType":{"type":"string","enum":["Bearer"]},"expiresIn":{"type":"integer","minimum":60},"expiresAt":{"type":"integer","description":"Unix epoch (secondi)."}}},"AskRequest":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":4,"description":"Domanda in italiano sul corpus normativo.","example":"Quanti giorni di ferie ho come docente di ruolo?"},"history":{"type":"array","items":{"type":"string"},"description":"Turni precedenti dell’utente nella stessa chat: chiarimenti, seguito in testo libero, o la domanda da approfondire."}}},"AskResponse":{"type":"object","required":["question","found","coverage","reliability","statements","sources","relatedQuestions","topics"],"properties":{"question":{"type":"string"},"found":{"type":"boolean"},"coverage":{"type":"string","enum":["alta","media","bassa"]},"reliability":{"type":"object","required":["score","label","reason","tokenCoverage","aspectCoverage","exactness"],"properties":{"score":{"type":"integer","minimum":0,"maximum":100,"description":"0–100, basato su quanto il match con i claim è esatto e su quante parti della domanda sono coperte."},"label":{"type":"string","enum":["alta","media","bassa"]},"reason":{"type":"string"},"tokenCoverage":{"type":"number","minimum":0,"maximum":1},"aspectCoverage":{"type":"number","minimum":0,"maximum":1},"exactness":{"type":"number","minimum":0,"maximum":1}}},"statements":{"type":"array","items":{"type":"object","required":["claimId","text","quote","citations"],"properties":{"claimId":{"type":"string"},"text":{"type":"string"},"quote":{"type":"string"},"citations":{"type":"array","items":{"type":"object","required":["id","title","publisher","year","kind","pinpoint"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"publisher":{"type":"string"},"year":{"type":"string"},"kind":{"type":"string","enum":["legge","decreto","ccnl","ccni","om","portale","giurisprudenza","guida","articolo"]},"url":{"type":"string","format":"uri"},"pinpoint":{"type":"string","description":"Articolo, comma, pagina, sezione o paragrafo di un articolo di settore."}}}}}}},"sources":{"type":"array","items":{"type":"object","required":["id","title","publisher","year","kind","pinpoint"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"publisher":{"type":"string"},"year":{"type":"string"},"kind":{"type":"string","enum":["legge","decreto","ccnl","ccni","om","portale","giurisprudenza","guida","articolo"]},"url":{"type":"string","format":"uri"},"pinpoint":{"type":"string","description":"Articolo, comma, pagina, sezione o paragrafo di un articolo di settore."}}}},"relatedQuestions":{"type":"array","items":{"type":"string"}},"topics":{"type":"array","items":{"type":"object","required":["id","slug","title","category"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"title":{"type":"string"},"category":{"type":"string"}}}},"refusalReason":{"type":"string"},"narrative":{"type":"string"},"narrativeStatus":{"type":"string","enum":["ok","unavailable","rejected","error"]},"narrativeNote":{"type":"string"},"canExpandDetails":{"type":"boolean","description":"Se true, la sintesi è la prima risposta breve: il corpus ha altri passaggi e si può chiedere «Sì, voglio più dettagli»."},"narrativeDepth":{"type":"string","enum":["brief","full"],"description":"brief = prima risposta corta; full = approfondimento richiesto dall’utente."},"needsClarification":{"type":"boolean","description":"Se true, la domanda è troppo generica: Norma chiede un dettaglio prima di cercare nel corpus."},"clarification":{"type":"object","required":["slot","prompt","options"],"properties":{"slot":{"type":"string","enum":["leaveType","permessoType","employment"]},"prompt":{"type":"string"},"options":{"type":"array","items":{"type":"object","required":["id","label","question"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"question":{"type":"string"}}}}}},"resolvedQuestion":{"type":"string","description":"Domanda ricostruita unendo i chiarimenti precedenti."}}},"ExplainRequest":{"type":"object","required":["question","statements"],"properties":{"question":{"type":"string","minLength":4},"statements":{"type":"array","minItems":1,"maxItems":12,"items":{"type":"object","required":["claimId"],"properties":{"claimId":{"type":"string","description":"Identificativo di un claim già restituito da `/api/ask`. Testo e virgolettato vengono riletti dal corpus server-side."}}}}}},"ExplainResponse":{"type":"object","required":["ok","status"],"properties":{"ok":{"type":"boolean"},"narrative":{"type":"string"},"status":{"type":"string","enum":["ok","unavailable","rejected","error"]},"note":{"type":"string"}}},"LlmStatusResponse":{"type":"object","required":["configured","model","providerHint"],"properties":{"configured":{"type":"boolean"},"model":{"type":["string","null"]},"providerHint":{"type":["string","null"],"enum":["groq","openrouter","openai-compatible",null]}}},"WatchFinding":{"type":"object","required":["kind","title","url","feedId","matchedCorpusIds","reasons","actType"],"properties":{"kind":{"type":"string","enum":["new_relevant","already_in_corpus","unreachable_source"]},"title":{"type":"string"},"url":{"type":"string","format":"uri"},"feedId":{"type":"string"},"publishedAt":{"type":"string","format":"date"},"matchedCorpusIds":{"type":"array","items":{"type":"string"}},"reasons":{"type":"array","items":{"type":"string"}},"actType":{"type":"string"}}},"WatchReport":{"type":"object","required":["checkedAt","since","lookbackDays","feeds","findings","uningestedCirculars","summary"],"properties":{"checkedAt":{"type":"string","format":"date-time"},"since":{"type":"string","format":"date"},"lookbackDays":{"type":"integer","minimum":1},"feeds":{"type":"array","items":{"type":"object","required":["id","title","ok","itemCount"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"ok":{"type":"boolean"},"itemCount":{"type":"integer"},"error":{"type":"string"}}}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/WatchFinding"}},"uningestedCirculars":{"type":"array","items":{"type":"object","required":["id","title","url"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","format":"uri"}}}},"summary":{"type":"object","required":["newRelevant","alreadyKnown","unreachable","feedErrors"],"properties":{"newRelevant":{"type":"integer"},"alreadyKnown":{"type":"integer"},"unreachable":{"type":"integer"},"feedErrors":{"type":"integer"}}},"pullRequest":{"type":"object","description":"Presente se il cron ha tentato di aprire o aggiornare la PR di catalogazione.","properties":{"created":{"type":"boolean"},"updated":{"type":"boolean"},"skipped":{"type":"boolean"},"reason":{"type":"string"},"number":{"type":"string"},"url":{"type":"string"}}}}},"UnauthorizedError":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["unauthorized"]},"message":{"type":"string"}}}}},"paths":{"/api/session":{"post":{"tags":["Auth"],"summary":"Emette un access token stateless","description":"Nessuna sessione persistita. Il token è un JWT HS256 con `sub=norma-web`, `iat`, `exp`, `jti`.","operationId":"createSession","responses":{"200":{"description":"Token emesso.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}}}}}},"/api/ask":{"post":{"tags":["RAG"],"summary":"Risponde in italiano con passaggi citati","description":"Recupera più nuclei del corpus (domande composte), produce una sintesi breve in linguaggio naturale e uno score di affidabilità. Se il corpus ha altri passaggi, chiede se servono più dettagli. Se la domanda è troppo generica (es. un congedo senza tipologia), chiede un chiarimento invece di mescolare i passaggi. Fuori corpus → refusal. Se l'LLM è configurato, riformula la sintesi restando sui passaggi.","operationId":"askQuestion","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskRequest"}}}},"responses":{"200":{"description":"Risposta citata o refusal controllato.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskResponse"}}}},"400":{"description":"Domanda troppo corta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskResponse"}}}},"401":{"description":"Token assente o non valido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}}}}},"/api/explain":{"post":{"tags":["LLM"],"summary":"Riformula claim già citati","description":"Riformula i claim recuperati in italiano naturale. Il server rilegge i claim dal corpus usando solo gli identificativi e richiede almeno una citazione `[n]` valida in ogni frase.","operationId":"explainAnswer","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainRequest"}}}},"responses":{"200":{"description":"Narrativa ok, unavailable, rejected o error (sempre JSON).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainResponse"}}}},"400":{"description":"Input incompleto.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainResponse"}}}},"401":{"description":"Token assente o non valido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}}}}},"/api/llm-status":{"get":{"tags":["LLM"],"summary":"Stato configurazione LLM","operationId":"getLlmStatus","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Configurazione senza esporre la chiave.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LlmStatusResponse"}}}},"401":{"description":"Token assente o non valido.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}}}}},"/api/cron/normative-watch":{"get":{"tags":["Ops"],"summary":"Controllo giornaliero della normativa","description":"Confronta gli atti pubblicati da MIM e Gazzetta Ufficiale con il corpus. Non ingerisce claim: se è configurato un token Origin, apre o aggiorna una PR di catalogazione (`ingested=no`). Chiamato da Vercel Cron con `Authorization: Bearer $CRON_SECRET`.","operationId":"watchNormativa","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Rapporto del controllo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchReport"}}}},"401":{"description":"Cron non autorizzato.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}}}}},"/api/openapi":{"get":{"tags":["Meta"],"summary":"Documento OpenAPI 3.1","operationId":"getOpenApi","responses":{"200":{"description":"Specifica OpenAPI in JSON.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}}}}