Repository navigation
fix(drift): verifica HTTP degli URL consumati — l'inventario non è autorevole - #437
Merged
Merged
Conversation
…torevole Il matcher `url` confrontava l'URL consumato con un solo `distribution_url` per item d'inventario. Due falsi positivi già in produzione: - AGID/indice-pa: l'inventario descrive `catalogocloud.agid.gov.it`, il repo consuma `indicepa.gov.it/ipa-dati/...` → 8 "missing" su URL che rispondono 200 - Consip: l'inventario tiene un resource per package, il repo consuma più anni → 15 "missing" su URL che rispondono 200 Ora `check_referenced_items` accetta un `url_probe` opzionale: se l'inventario conferma l'URL → matched (nessuna rete); altrimenti decide la verifica dell'URL consumato — 2xx → matched, risposta di errore → missing (drift vero), errore di rete → unknown (nessun falso drift). Con inventario vuoto (fonti radar-only) il check funziona solo grazie al probe. Senza probe il comportamento resta quello storico. - drift.py: `url_probe` (tri-stato) in `_url_state`/`_state_for`/ `check_referenced_items`; l'euristica inventario diventa `_url_state_from_inventory` - build.py: `_UrlProbe` adatta `probe_pattern` al tri-stato, con cache per run e UA da `[tool.observatory]` (fonti con WAF) - pattern_probes.py: `probe_pattern` accetta `user_agent` - runbook: come funziona il drift degli URL consumati Misurato: indice-pa 8 missing → 0 (9 verificati via HTTP); Consip 15 → 1. Il ref rimasto è drift reale: `consumi-in-convenzione-2023.csv` risponde 404 e il portale espone solo 2024/2025/2026. Verifiche: 435 passed / 2 skipped, mypy e ruff puliti.
Due difetti trovati verificando il fix su casi reali (open-consip, indice-pa):
1. **URL templati sondati letteralmente.** Un ref con placeholder
(`...index.php?tree={year}CIVATOT0201`) veniva passato al probe così com'è:
l'URL concreto lo genera la pipeline a runtime, quindi la verifica dava
`missing` per costruzione. Colpiva 30 URL in 6 repo (es. dichiarazioni-italia,
16 ref). Ora i ref templati non si sondano: resta l'esito dell'inventario.
2. **Il probe poteva sollevare.** `probe_pattern` leggeva il corpo con
`resp.content`: su uno stream resettato dal peer (`ChunkedEncodingError`)
l'eccezione usciva dal probe e avrebbe fatto fallire il build del catalog.
Ora la lettura è protetta e un fallimento è "non concludente" (`http_code`
`"-"`), coerente con gli errori di rete.
In più, la verifica di esistenza degli URL consumati non legge più il corpo
(`read_body=False`): basta lo status, evitando di scaricare N file grandi per
un controllo di esistenza. `content_length` resta `None` in quel caso.
Verifiche: 432 passed / 2 skipped, mypy e ruff puliti; reale su open-consip
(21 checked, 20 matched, 1 missing = il 404 del 2023) e indice-pa (9/9).
Un blocco WAF o un rate limit non prova che la risorsa sia stata rimossa: mappare "qualunque codice HTTP di errore" a `missing` produceva falsi drift proprio sulle fonti che ci proteggono con 403 (ANAC, MUR, Consip hanno già un user_agent dedicato in config per questo motivo). Ora i codici 401/403/407/429 danno `unknown` (non concludente), come gli errori di rete. Restano `missing` i 404/410 e gli altri errori definitivi. - build.py: `_INCONCLUSIVE_HTTP_CODES` + mapping in `_UrlProbe._probe` - runbook: semantica aggiornata (codici non concludenti, ref templati) - test: caso 403 nell'adattatore (atteso `None`) Verifiche: 432 passed / 2 skipped, mypy e ruff puliti.
7 tasks done
Gabrymi93
added a commit
that referenced
this pull request
Oct 28, 2026
…atch per kind (#439) * fix(drift): `missing` solo con prova di assenza — regola unica, non patch per kind Il drift dichiarava `missing` quando un ref non si trovava nell'inventario, assumendo che l'inventario descrivesse tutto ciò che la fonte pubblica. Non è vero per 3 dei 5 kind, e ogni kind che poggia sull'assunzione perdeva: - `ckan_resource`: l'inventario tiene UNA distribuzione per item, ma un package CKAN ne ha in media decine → open-mur segnava 9 resource "sparite" che invece esistono (verificato con `resource_show`); - `ckan_dataset`: il matcher toglieva il placeholder e faceva `startswith`, che funziona solo con la parte fissa in testa: `{year}-nome` non matchava mai → 1 falso positivo su open-mur; - `url`: già risolto con la verifica HTTP (#437), stesso principio. Ora la regola è una sola, dichiarata in `drift.ABSENCE_EVIDENCE`: `missing` solo con prova positiva di assenza — inventario **completo** per quel kind, oppure verifica diretta dell'item. Un kind non dichiarato non può dichiarare `missing`: al massimo `unknown`. - drift.py: tabella `ABSENCE_EVIDENCE` (load-bearing in `_state_for`), `_match_ckan_dataset` con placeholder come wildcard, nuovo `_ckan_resource_state` (probe o `unknown`) - build.py: `_CkanResourceProbe` — `resource_show` sull'endpoint CKAN della fonte, cache per run, UA di config; 403/429/rete → non concludente - runbook: la regola della prova di assenza, con la tabella per kind Misurato: open-mur **10 missing → 0** (13/13 matched, 9 via `resource_show`); nessuna regressione su appalti-pubblici (13/13), indice-pa (9/9), e il drift **reale** di Consip resta segnalato (1 missing, il 404 del 2023). Verifiche: 452 passed / 2 skipped, mypy e ruff puliti. * test(drift): la tabella è il registro dei kind — gate a uguaglianza Follow-up dei finding della review su #439. - `_MATCHERS` non elenca più `ckan_resource` (kind a tre stati): ogni kind ha un solo percorso - `_state_for` consulta `ABSENCE_EVIDENCE` come **primo** passo: un kind non dichiarato non entra nel check, e la prova dichiarata decide se l'assenza può diventare `missing` - gate a **uguaglianza** (dichiarato ⇔ instradabile) invece che a sottoinsieme: copre anche "dichiarato ma non instradabile" - nuovo test: un kind fuori tabella non entra nel check Comportamento invariato, verificato su open-mur (13/13, 9 via resource_show), appalti-pubblici (13/13) e open-consip (1 missing reale). Verifiche: 453 passed / 2 skipped, mypy e ruff puliti.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sintesi
Il matcher
urldel referenced-item drift confrontava l'URL consumato con un solodistribution_urlper item d'inventario. L'inventario non è autorevole per gli URL dihttp_file: può descrivere un altro endpoint dello stesso dato, o una sola distribuzione per item. Risultato: due falsi positivi già in produzione, su URL che rispondono 200.Contesto collegato
Nessuna issue: emerso misurando durante l'onboarding di
open-consip(fase 1 della decentralizzazione). Motivazione dell'assenza di issue: il difetto non era segnalato, è stato trovato confrontando il drift con la realtà HTTP.Cosa cambia
Checklist
pytest tests/passa (429 passed, 2 skipped)ruff check .passamypy scripts/ so_mcp/passaso-catalog --dry-runsuindice-paeopen-consip)Verifica
Causa dei falsi positivi:
agiddescrivecatalogocloud.agid.gov.it, il repo consumaindicepa.gov.it/ipa-dati/...→ endpoint diversi, stesso dato;Il ref rimasto su Consip è drift reale:
consumi-in-convenzione-2023.csvrisponde 404 e il portale espone solo 2024/2025/2026.Semantica: inventario conferma l'URL →
matched(nessuna rete); altrimenti decide la verifica HTTP — 2xx →matched, risposta di errore →missing, errore di rete →unknown(un timeout non deve produrre un falso drift). Con inventario vuoto (fonti radar-only, es.dcl-bologna) il check ora gira, dove prima erachecked=0. Senzaurl_probeil comportamento resta quello storico.Note per chi revisiona
missingsi restringe: spariscono i falsi positivi, restano i drift veri. Ununknownin più è preferibile a un drift falso.unknown, non drift: un blocco WAF o un rate limit non prova che la risorsa sia stata rimossa. Conta sulle fonti che ci proteggono con 403 (ANAC, MUR, Consip hanno già unuser_agentdedicato).{year}) non si sondano: l'URL concreto lo genera la pipeline a runtime, quindi sondarli darebbemissingper costruzione. Resta l'esito dell'inventario. Vale per 30 URL in 6 repo (es.dichiarazioni-italia, 16 ref).read_body=False): basta lo status. La lettura è comunque protetta — su uno stream resettato il probe ora è "non concludente" (http_code"-") invece di sollevare, che avrebbe fatto fallire il build.registry/catalog.jsoncommittati nei repo si ripuliscono al primo run dopo il merge (cron settimanale): fino ad allora contengono i missing vecchi.[tool.observatory], per le fonti dietro WAF.