Repository navigation
feat(catalog): referenced-item drift — verifica upstream vs inventario - #429
Conversation
Per ogni dataset con upstream refs, controlla che gli item referenziati
esistano ancora nell'inventario della fonte. Se ne sparisce uno →
trigger referenced_item_missing nel verdict (label REFERENCED_MISSING).
Match per kind:
- dataflow: item_id == flow ID
- ckan_dataset: item_name == dataset name (gestisce placeholder {year})
- ckan_resource: distribution_url contiene resource ID
- url: prefix match sul path statico (rimuove {year} e query)
- script_type: keyword matching (il tipo e' un ID logico, non un
suffisso diretto — es. 'ricorsi-pervenuti-class' matcha
'ricorsi-pervenuti-per-classificazione' via keywords)
Verificato su GA: 11/11 upstream matchati, 0 missing, verdict STABLE.
+11 test (match per kind, placeholder, missing, mixed).
…to unknown
Il matcher `url` troncava l'URL a '?' e faceva prefix match sul path: per le
fonti che servono il dato via query-string il prefisso statico e' tutto il
portale. Due conseguenze, entrambe misurate sull'inventario reale di MEF
analisi_stat (fonte di dichiarazioni-italia):
- falso positivo: 5 dei 7 dataset dichiarati REFERENCED_MISSING pur essendo
sani (inventario homepage, URL del dataset su .../public/index.php);
- falso negativo: con l'inventario dell'albero, qualunque export parte da
index.php → 'base in url' sempre vero, il drift non viene mai rilevato.
Ora:
- match sull'URL completo con i placeholder {year} come wildcard, con fallback
sul solo path quando la query e' un parametro di serializzazione (ECB
?format=csvdata) → la query non e' piu' ne' identita' per forza ne' rumore;
- esito a tre stati: matched / missing / unknown. 'unknown' = l'inventario non
copre l'area del ref (o il pattern matcha tutto l'inventario, quindi non
discrimina): niente falso allarme, niente silenzio.
- area coperta (→ missing) se il prefisso statico del ref esiste nell'inventario
oppure se il ref condivide con alcuni item un prefisso che entra nel path ma
non con tutti.
Verifiche (inventari reali dei repo del workspace, old vs new):
giustizia-amministrativa 11/0 → 11/0/0 eurostat 34/0 → 34/0/0
oecd-benchmark 11/0 → 11/0/0 appalti-pubblici 12/0 → 12/0/0
sistema-bancario 4/0 → 4/0/0 (ECB: fix del falso missing da query)
open-mur 3/10 → 3/10/0 (invariato)
indice-pa 0/9 → 0/8/1 (drift preservato, 1 ref non coperto → unknown)
opencivitas 3/1 → 1/0/3 (2 'matched' vacui + 1 missing non concludibile)
MEF: inventario homepage 2 matched / 0 missing / 5 unknown (era 5 falsi
missing); albero con vista rimossa → missing rilevato; albero completo → 7/0/0
5 test nuovi su query-string, serializzazione, match vacuo e area non coperta.
Update: matcher
|
| scenario | prima | ora |
|---|---|---|
| inventario = homepage magnet (147 item reali) | 2 matched / 5 missing (falsi) | 2 / 0 missing / 5 unknown |
inventario = albero, vista CIVATOT0201 rimossa |
matched (drift non rilevato) | missing rilevato |
| inventario = albero completo + homepage | matched | 7 / 0 / 0 |
Cosa cambia
- Match: URL completo con
{year}come wildcard, con fallback sul solo path quando la query è un parametro di serializzazione (ECB?format=csvdata→ altrimenti 4 falsimissingsusistema-bancario). Un pattern che matcha tutti gli item non discrimina →unknown, nonmatched. - Tre stati:
matched/missing(drift) /unknown(inventario che non copre l'area). Nuovo camporeferenced_items.unknownnello schema; il triggerreferenced_item_missingcontinua a scattare solo sumissing. - Area coperta (→
missing) se il prefisso statico del ref esiste nell'inventario, oppure se il ref condivide con alcuni item un prefisso che entra nel path ma non con tutti.
Impatto sui repo con inventario locale (old → new)
giustizia-amministrativa 11/0 → 11/0/0 eurostat 34/0 → 34/0/0
oecd-benchmark 11/0 → 11/0/0 appalti-pubblici 12/0 → 12/0/0
sistema-bancario 4/0 → 4/0/0 open-mur 3/10 → 3/10/0 (invariato)
indice-pa 0/9 → 0/8/1 opencivitas 3/1 → 1/0/3
indice-pa: 8 drift su 9 preservati, 1 ref (.../datastore/dump/<uuid>?bom=True) non coperto dall'inventario → unknown.
opencivitas: i 2 matched erano vacui (ref = directory /documents/20118/0/ presente in tutti i 367 item) → unknown; l'1 missing era docs.opencivitas.it, host non presente nell'inventario → unknown.
Test
5 nuovi in tests/test_catalog_drift.py (query-string che discrimina, vista rimossa → drift, query di serializzazione, pattern vacuo, area non coperta) + i 2 esistenti che asserivano il dict esatto aggiornati. pytest tests/, ruff, mypy verdi in locale.
Nota: la CI di questo branch è rossa su mypy per _validate_base.py:197 (duckdb.sql() tipizzato Optional da duckdb 2.0) — non c'entra con questa modifica, è lo stesso problema pre-esistente fixato in #435, e con mypy rosso pytest non gira. Va risolto per poter validare questa PR in CI (vedi commento successivo).
Cherry-pick da #435: serve a far girare lint/mypy/pytest su questa PR. La CI installa duckdb 2.0.0 (il pin è >=1.5.3) i cui stubs tipizzano duckdb.sql() come Optional: mypy falliva su _validate_base.py con 'union-attr', e con mypy rosso pytest viene saltato. Il venv locale ha duckdb 1.5.6, quindi il problema non è riproducibile in locale: verificato sul log CI (Downloading duckdb-2.0.0).
|
Aggiornamento: risolto il blocco CI.
|
…atch Review #429 (finding Medium). Il matcher a keyword (`>=2 token`) era troppo largo: un token generico bastava a restare 'matched' anche dopo la rimozione dell'intera famiglia referenziata. Prova avversariale sull'inventario reale openga (436 item): ref ricorsi-pervenuti-class → 155 match (ricorsi 248, pervenuti 93, class 124) rimossi tutti i package 'pervenuti' → restava 'matched' (via ricorsi+class) ref ricorsi-tipo-decisione → 93 match rimossi tutti i 'tipo-di-decisione' → restava 'matched' (via ricorsi+tipo) Ora `_script_type_state` ha tre esiti, coerenti con il matcher URL: - matched: un item contiene TUTTI i token del ref; - missing: nessun item li contiene tutti ma almeno un token compare ancora (la famiglia c'e', la variante no) — o ref mono-token assente (la famiglia stessa e' sparita, caso motivante 'package *-sentenze'); - unknown: ref multi-token di cui nessun token compare nell'inventario. Verifiche: GA normale 11/11 matched (invariato); rimozione di 'pervenuti', 'tipo-di-decisione', 'sentenze', 'sentenze-brevi', 'ricorsi-definiti' → tutti missing (prima 2/11 restavano matched). Nessuna regressione sugli altri repo: sistema-bancario 4/4, eurostat 34/34, oecd 11/11, appalti 12/12, open-mur 3 matched/10 missing, indice-pa 8/1, opencivitas 1/3 — identici a prima. Review #429 (finding Medium) — test di integrazione mancanti: nuovo TestReferencedItemsIntegration in test_catalog_build.py (4 test): campo referenced_items nell'e2e, caso missing con trigger/label/score/next_action, label non riscritta quando non era STABLE, coesistenza drift + new_content. 3 test unit nuovi sul matcher script_type (multi-token, famiglia rimossa, vocabolario assente).
Fix dei due finding bloccanti (
|
| ref | prima | token (coverage) | famiglia rimossa | prima | ora |
|---|---|---|---|---|---|
ricorsi-pervenuti-class |
155 match | ricorsi 248, pervenuti 93, class 124 | tutti i pervenuti |
matched | missing |
ricorsi-tipo-decisione |
93 match | ricorsi 248, tipo 93, decisione 31 | tutti i tipo-di-decisione |
matched | missing |
Nuova _script_type_state, a tre esiti coerenti col matcher URL:
matched— un item contiene tutti i token del ref;missing— nessun item li contiene tutti ma almeno un token compare ancora (la famiglia c'è, la variante no), oppure ref mono-token assente (la famiglia stessa è sparita: è il caso motivante dei package*-sentenze);unknown— ref multi-token di cui nessun token compare nell'inventario (vocabolario non allineato / inventario che non copre).
Verifiche: GA normale 11/11 matched (invariato) e, rimuovendo le famiglie pervenuti, tipo-di-decisione, sentenze, sentenze-brevi, ricorsi-definiti → tutti missing. Nessuna regressione sugli altri repo: sistema-bancario 4/4, eurostat 34/34, oecd-benchmark 11/11, appalti-pubblici 12/12, open-mur 3 matched/10 missing, indice-pa 8 missing/1 unknown, opencivitas 1/3 — identici a prima del fix.
2. Test di integrazione sul verdict
Nuovo TestReferencedItemsIntegration in tests/test_catalog_build.py (4 test):
test_end_to_end_records_referenced_items—referenced_items == {checked: 1, matched: 1, missing: [], unknown: []}, nessun trigger;test_missing_upstream_sets_trigger_and_label— ref assente →trigger referenced_item_missing,label REFERENCED_MISSING,score referenced_missing,next_action review missing upstream items;test_label_not_overwritten_when_not_stable— con verdictPARTIALLY_SCOPEDil drift aggiunge il trigger ma non riscrive la label;test_drift_and_new_content_coexist— drift + nuovo contenuto: entrambi i trigger, labelREFERENCED_MISSING.
Il quarto test documenta la precedenza reale, che è l'opposto di quanto suggerisce il body ("non sovrascrive NEW_CONTENT"): il blocco drift gira prima di quello dei pattern probes e ognuno promuove la label solo da STABLE, quindi con entrambi i segnali vince REFERENCED_MISSING e new_content_available resta tra i trigger. Nessuna informazione persa, ma se la precedenza voluta è l'altra va invertita la promozione — dimmi.
pytest tests/ 412 passed / 2 skipped, ruff check e mypy verdi; CI test verde.
Non affrontati (e perché)
ckan_datasetprefisso-largo (Info): nessun impatto sui ref attuali; toccherebbe anche il caso{year}-<slug>(open-mur) che oggi è giàmissingper altri motivi — meglio una PR dedicata con il suo caso reale.- Schema
referenced_itemspermissivo (Info):build.pyfornisce semprechecked/matched; irrigidire è backward-incompatible per i catalog già scritti. - Enum
REFERENCED_MISSINGin lab-dashboard (Low): follow-up fuori da questa PR.
Sintesi
Il catalog ora verifica che gli item upstream referenziati dai dataset esistano ancora nell'inventario della fonte. Se ne sparisce uno → trigger
referenced_item_missingnel verdict.Perché
Il mapping
upstreamdocumentava la dipendenza ma non la verificava. Per GA: 12 dataset consumano tipi comesentenze— se il portale rimuove i package*-sentenze, il catalog non lo segnalava.Come funziona
Per ogni dataset con upstream refs,
check_referenced_items()matcha contro l'inventario:dataflowitem_id== flow IDckan_datasetitem_name== dataset name (gestisce{year})ckan_resourcedistribution_urlcontiene resource IDurlscript_typeSe missing →
referenced_item_missingnel verdict, labelREFERENCED_MISSING(non sovrascriveNEW_CONTENT).Verifiche
referenced_items+ enumREFERENCED_MISSING