Livello dei casi 1/5 — provenienza: una riga di prova per ogni chiamata a monte #4

Open
NicolasMoerynck wants to merge 1 commit from case-layer-1-provenienza into develop

Branch case-layer-1-provenienza → develop. Circa 1.370 righe: 476 di codice (445 sono provenance.py), 893 di test.
Spento di default: senza PROVENANCE_LOG non scrive niente e non cambia niente.

Cosa fa

  • provenance.py (nuovo). record() scrive una riga JSON per ogni chiamata HTTP a monte: modulo, tool, URL, richiesta, stato, retrieved_at, byte e sha256 dei byte grezzi, case_id. Va nel file indicato da PROVENANCE_LOG. Se imposti anche PROVENANCE_BLOBS, salva pure i byte grezzi, per hash (<sha256>.bin). read_records(case_id) rilegge le righe di un caso, e fa il parsing solo delle righe che contengono quel case_id.
  • Un hook per punto di uscita, una chiamata ciascuno, dopo che i byte sono arrivati e prima del parsing:
    • arpa_taranto._get_csv;
    • i tre wrapper async di eea_air_quality (_get_json, _post_json, _post_parquet);
    • ispra_air_quality.fetch_wfs, dentro la funzione, perché correlation la importa per nome;
    • correlation._fetch_eea;
    • SupersetClient.execute_sql: qui l'hash è del JSON canonico delle righe restituite, e il record lo dice (sha256_of: "canonical_json").
  • CaseIdMiddleware in server.py, montato dopo MetricsMiddleware: legge l'header X-Case-Id e lo mette su ogni riga di quella richiesta. È un middleware ASGI puro. Oggi nessuno manda l'header, quindi case_id è null.
  • Dockerfile: una riga, COPY provenance.py .. I domini lo importano al caricamento.

Perché

  • Oggi nessun risultato dei tool aria porta un URL, un'ora di recupero o un hash: gli URL finiscono solo su stdout ([arpa] GET …). Un valore di benzene in una risposta non si riesce a ricondurre ai byte da cui viene.
  • Rifare la chiamata dopo non basta. Il WFS ISPRA (dati_nrt_informambiente_mv) tiene circa 8 giorni e omette le ore invalidate senza dirlo, e gli archivi EEA e ARPA si correggono. I byte fanno da prova solo se li fissi quando arrivano.

Cosa NON cambia

  • Senza PROVENANCE_LOG, record() esce prima di fare qualsiasi cosa: nessun file, nessun hash. È lo stato di default.
  • L'output di ogni tool resta identico byte per byte, con il log acceso o spento. Il test lo verifica su ARPA, EEA, ISPRA, correlation e NAVCO/Superset. I valori di ritorno non cambiano.
  • record() non solleva mai. Con un disco pieno o un percorso sbagliato si perde la riga, mai la risposta. Anche questo è testato: con il sink rotto, output identici.
    • Una richiesta con oggetti che il JSON non conosce si scrive come testo; solo una richiesta circolare fa perdere la riga.
    • Se l'ultima riga del file è rimasta a metà (un processo interrotto mentre scriveva), la riga nuova parte su una riga sua, e in lettura la mezza riga si salta.
    • Un sink rotto stampa un avviso una volta sola per processo, distinto per le righe e per i blob: «blob not kept (line written)» quando la riga c'è e manca solo il blob, «record lost» quando si perde la riga.
  • Non cambiano nemmeno i prompt, il modello, la temperatura, la UI pubblica, audit_answer e findings.py. Nessuna nuova dipendenza: solo stdlib.

Come si prova

PYENV_VERSION=pck-ai pyenv exec python examples/provenance_test.py   # 104 controlli
PYENV_VERSION=pck-ai pyenv exec python examples/findings_test.py     # 23, invariato

Il test gira offline: blocca la rete e sostituisce httpx, fastmcp, pyarrow, starlette e prometheus con finti. Controlla anche che ogni hook sia uno solo per punto di uscita e che i file toccati passino il parser di Python 3.11.

Decisioni per te

  1. Se e dove accenderlo in produzione, e per quanto. PROVENANCE_LOG va messo su un volume persistente del pod. Crescono tutti e due i file:
    • le righe, una per ogni chiamata a monte, anche per il traffico pubblico (con case_id nullo): nelle prove offline una riga pesa fra 0,5 e 1,2 KB. stamp_case (MR3) scorre tutto il file a ogni risposta timbrata, quindi conviene una rotazione. Si può fare senza fermare niente: record() riapre il file a ogni scrittura, e a stamp_case servono solo le righe del turno in corso;
    • i blob, con ogni download EEA (Parquet): o si decide per quanto tenerli, o PROVENANCE_BLOBS resta vuota e si tengono solo le righe.
  2. L'header X-Case-Id.
    • Il valore lo manderebbe l'assistente di PeaceLink, con una modifica a parte nel suo repo. La proposta è <share_id>:<turno>:<nonce>, con un nonce nuovo a ogni turno: il solo <share_id>:<turno> si ripete, per esempio dopo «Cancella chat».
    • L'header è preso così com'è: il server non distingue l'assistente da un altro client. Sulla rotta interna della webapp va bene. Sulla rotta pubblica, dietro il login, un client autorizzato potrebbe scrivere righe sotto il caso di un altro. Per questo ti propongo che l'ingress pubblico lo tolga, per esempio con un middleware Traefik headers solo su quella rotta, con customRequestHeaders: {X-Case-Id: ""} (un valore vuoto toglie l'header). La rotta interna resta com'è.
    • Se preferisci un altro nome, si cambia in provenance.py, in due punti.
Branch `case-layer-1-provenienza` → `develop`. Circa 1.370 righe: 476 di codice (445 sono `provenance.py`), 893 di test. Spento di default: senza `PROVENANCE_LOG` non scrive niente e non cambia niente. ## Cosa fa - **`provenance.py`** (nuovo). `record()` scrive una riga JSON per ogni chiamata HTTP a monte: modulo, tool, URL, richiesta, stato, `retrieved_at`, byte e sha256 dei byte grezzi, `case_id`. Va nel file indicato da `PROVENANCE_LOG`. Se imposti anche `PROVENANCE_BLOBS`, salva pure i byte grezzi, per hash (`<sha256>.bin`). `read_records(case_id)` rilegge le righe di un caso, e fa il parsing solo delle righe che contengono quel `case_id`. - **Un hook per punto di uscita**, una chiamata ciascuno, dopo che i byte sono arrivati e prima del parsing: - `arpa_taranto._get_csv`; - i tre wrapper async di `eea_air_quality` (`_get_json`, `_post_json`, `_post_parquet`); - `ispra_air_quality.fetch_wfs`, dentro la funzione, perché `correlation` la importa per nome; - `correlation._fetch_eea`; - `SupersetClient.execute_sql`: qui l'hash è del JSON canonico delle righe restituite, e il record lo dice (`sha256_of: "canonical_json"`). - **`CaseIdMiddleware`** in `server.py`, montato dopo `MetricsMiddleware`: legge l'header `X-Case-Id` e lo mette su ogni riga di quella richiesta. È un middleware ASGI puro. Oggi nessuno manda l'header, quindi `case_id` è `null`. - **`Dockerfile`**: una riga, `COPY provenance.py .`. I domini lo importano al caricamento. ## Perché - Oggi nessun risultato dei tool aria porta un URL, un'ora di recupero o un hash: gli URL finiscono solo su stdout (`[arpa] GET …`). Un valore di benzene in una risposta non si riesce a ricondurre ai byte da cui viene. - Rifare la chiamata dopo non basta. Il WFS ISPRA (`dati_nrt_informambiente_mv`) tiene circa 8 giorni e omette le ore invalidate senza dirlo, e gli archivi EEA e ARPA si correggono. I byte fanno da prova solo se li fissi quando arrivano. ## Cosa NON cambia - **Senza `PROVENANCE_LOG`**, `record()` esce prima di fare qualsiasi cosa: nessun file, nessun hash. È lo stato di default. - **L'output di ogni tool resta identico byte per byte**, con il log acceso o spento. Il test lo verifica su ARPA, EEA, ISPRA, correlation e NAVCO/Superset. I valori di ritorno non cambiano. - **`record()` non solleva mai.** Con un disco pieno o un percorso sbagliato si perde la riga, mai la risposta. Anche questo è testato: con il sink rotto, output identici. - Una richiesta con oggetti che il JSON non conosce si scrive come testo; solo una richiesta circolare fa perdere la riga. - Se l'ultima riga del file è rimasta a metà (un processo interrotto mentre scriveva), la riga nuova parte su una riga sua, e in lettura la mezza riga si salta. - Un sink rotto stampa un avviso una volta sola per processo, distinto per le righe e per i blob: «blob not kept (line written)» quando la riga c'è e manca solo il blob, «record lost» quando si perde la riga. - **Non cambiano nemmeno** i prompt, il modello, la temperatura, la UI pubblica, `audit_answer` e `findings.py`. Nessuna nuova dipendenza: solo stdlib. ## Come si prova ```bash PYENV_VERSION=pck-ai pyenv exec python examples/provenance_test.py # 104 controlli PYENV_VERSION=pck-ai pyenv exec python examples/findings_test.py # 23, invariato ``` Il test gira offline: blocca la rete e sostituisce httpx, fastmcp, pyarrow, starlette e prometheus con finti. Controlla anche che ogni hook sia uno solo per punto di uscita e che i file toccati passino il parser di Python 3.11. ## Decisioni per te 1. **Se e dove accenderlo in produzione, e per quanto.** `PROVENANCE_LOG` va messo su un volume persistente del pod. Crescono tutti e due i file: - **le righe**, una per ogni chiamata a monte, anche per il traffico pubblico (con `case_id` nullo): nelle prove offline una riga pesa fra 0,5 e 1,2 KB. `stamp_case` (MR3) scorre tutto il file a ogni risposta timbrata, quindi conviene una rotazione. Si può fare senza fermare niente: `record()` riapre il file a ogni scrittura, e a `stamp_case` servono solo le righe del turno in corso; - **i blob**, con ogni download EEA (Parquet): o si decide per quanto tenerli, o `PROVENANCE_BLOBS` resta vuota e si tengono solo le righe. 2. **L'header `X-Case-Id`.** - Il valore lo manderebbe l'assistente di PeaceLink, con una modifica a parte nel suo repo. La proposta è `<share_id>:<turno>:<nonce>`, con un nonce nuovo a ogni turno: il solo `<share_id>:<turno>` si ripete, per esempio dopo «Cancella chat». - L'header è preso così com'è: il server non distingue l'assistente da un altro client. Sulla rotta interna della webapp va bene. Sulla rotta pubblica, dietro il login, un client autorizzato potrebbe scrivere righe sotto il caso di un altro. Per questo ti propongo che l'ingress pubblico lo tolga, per esempio con un middleware Traefik `headers` solo su quella rotta, con `customRequestHeaders: {X-Case-Id: ""}` (un valore vuoto toglie l'header). La rotta interna resta com'è. - Se preferisci un altro nome, si cambia in `provenance.py`, in due punti.
provenance.record() writes one JSON line per upstream HTTP fetch (module,
tool, url, request, status, retrieved_at, size and sha256 of the raw bytes,
case id) to the file named by PROVENANCE_LOG; with PROVENANCE_BLOBS it also
keeps the raw bytes, content-addressed. Without PROVENANCE_LOG it returns
before doing anything, and it never raises.

One hook per HTTP chokepoint: ARPA _get_csv, the three EEA async wrappers,
ISPRA fetch_wfs, correlation _fetch_eea and SupersetClient.execute_sql.
Return values are unchanged. CaseIdMiddleware, mounted after
MetricsMiddleware, carries the X-Case-Id request header into the records.
The Dockerfile copies provenance.py, which the domains import at load.

Test: examples/provenance_test.py (offline).
Author
Member

Se vuoi vedere dove va a finire, gli altri quattro pezzi sono già sul server come branch, senza merge request: case-layer-2-fondamenta, case-layer-3-timbro, case-layer-4-esame e case-layer-5-prosa (facoltativo). Ognuno parte dal precedente, quindi case-layer-5-prosa li contiene tutti. Le merge request le apro una alla volta, dopo questa o come preferisci.

Se vuoi vedere dove va a finire, gli altri quattro pezzi sono già sul server come branch, senza merge request: case-layer-2-fondamenta, case-layer-3-timbro, case-layer-4-esame e case-layer-5-prosa (facoltativo). Ognuno parte dal precedente, quindi case-layer-5-prosa li contiene tutti. Le merge request le apro una alla volta, dopo questa o come preferisci.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin case-layer-1-provenienza:case-layer-1-provenienza
git switch case-layer-1-provenienza

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch develop
git merge --no-ff case-layer-1-provenienza
git switch case-layer-1-provenienza
git rebase develop
git switch develop
git merge --ff-only case-layer-1-provenienza
git switch case-layer-1-provenienza
git rebase develop
git switch develop
git merge --no-ff case-layer-1-provenienza
git switch develop
git merge --squash case-layer-1-provenienza
git switch develop
git merge --ff-only case-layer-1-provenienza
git switch develop
git merge case-layer-1-provenienza
git push origin develop
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
peacelink/mcp-peacedata!4
No description provided.