Skip to content

feat(catalog): referenced-item drift — verifica upstream vs inventario - #429

Merged
Gabrymi93 merged 4 commits into
mainfrom
feat/referenced-drift
Oct 26, 2026
Merged

Gabrymi93 merged 4 commits into
mainfrom
feat/referenced-drift

Conversation

@Gabrymi93

Copy link
Copy Markdown
Member

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_missing nel verdict.

Perché

Il mapping upstream documentava la dipendenza ma non la verificava. Per GA: 12 dataset consumano tipi come sentenze — 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:

Kind Match
dataflow item_id == flow ID
ckan_dataset item_name == dataset name (gestisce {year})
ckan_resource distribution_url contiene resource ID
url prefix match sul path statico
script_type keyword matching (tipo = ID logico, non suffisso diretto)

Se missing → referenced_item_missing nel verdict, label REFERENCED_MISSING (non sovrascrive NEW_CONTENT).

Verifiche

  • GA: 11/11 upstream matchati, 0 missing, STABLE
  • 40 test verdi (drift + build + upstream)
  • Schema: campo referenced_items + enum REFERENCED_MISSING

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.
@Gabrymi93

Copy link
Copy Markdown
Member Author

Update: matcher url — match discriminante e stato unknown

Commit 93412bf. 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 è tutto il portale. Misurato sull'inventario reale di MEF analisi_stat (fonte di dichiarazioni-italia, 7 dataset):

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

  1. Match: URL completo con {year} come wildcard, con fallback sul solo path quando la query è un parametro di serializzazione (ECB ?format=csvdata → altrimenti 4 falsi missing su sistema-bancario). Un pattern che matcha tutti gli item non discrimina → unknown, non matched.
  2. Tre stati: matched / missing (drift) / unknown (inventario che non copre l'area). Nuovo campo referenced_items.unknown nello schema; il trigger referenced_item_missing continua a scattare solo su missing.
  3. 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).
@Gabrymi93

Copy link
Copy Markdown
Member Author

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).
@Gabrymi93

Copy link
Copy Markdown
Member Author

Fix dei due finding bloccanti (2813147)

1. script_type — un match parziale non è un match

Riprodotta la prova avversariale sull'inventario reale openga (436 item). Il matcher a keyword (>=2 token) restava matched per via dei token generici:

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 verdict PARTIALLY_SCOPED il drift aggiunge il trigger ma non riscrive la label;
  • test_drift_and_new_content_coexist — drift + nuovo contenuto: entrambi i trigger, label REFERENCED_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_dataset prefisso-largo (Info): nessun impatto sui ref attuali; toccherebbe anche il caso {year}-<slug> (open-mur) che oggi è già missing per altri motivi — meglio una PR dedicata con il suo caso reale.
  • Schema referenced_items permissivo (Info): build.py fornisce sempre checked/matched; irrigidire è backward-incompatible per i catalog già scritti.
  • Enum REFERENCED_MISSING in lab-dashboard (Low): follow-up fuori da questa PR.

@Gabrymi93
Gabrymi93 merged commit 9d42d0f into main Oct 26, 2026
1 check passed
@Gabrymi93
Gabrymi93 deleted the feat/referenced-drift branch October 26, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant