Skip to content

Commit 3090638

Browse files
aborrusoclaude
andauthored
fix(votes): aggancia all'atto i voti a codice secco via seduta monotematica (#71)
* fix(votes): aggancia all'atto i voti a codice secco via seduta monotematica Sulla seduta s19_689 (legge elettorale, 14/7/2026) manca del tutto ocd:rif_attoCamera e solo 2 voti su 12 citano l'atto nel testo: il voto sull'emendamento preferenze (188-187) restava senza bill_uri, invisibile a chi filtra per numero di atto. Terzo livello di fallback: se l'intera seduta verte su un solo atto, quello e' l'atto anche dei voti muti. Monotematicita' calcolata con query dedicata sulla seduta completa (mai sulle righe filtrate in memoria), unendo rif_attoCamera e i numeri citati nelle altre descrizioni. Mozioni e risoluzioni escluse: sono AIC a se' stanti. +3 test, nota wiki OKF, helper resolveActUris condiviso. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(votes): chiave numero atto per legislatura + test cross-legislatura Il numero d'atto si ripete tra legislature: senza chiavare la cache numero→URI anche sulla legislatura, una finestra a cavallo di 18 e 19 poteva agganciare un voto della 18 a un atto ac19_*. Documentato in LOG il residuo su --bill-code (filtra dc:description lato server, a monte dell'ereditarieta'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(log): +4 test, non +3 (review Copilot PR #71) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bill): ripulisce label/title/description da entita HTML e suffisso xsd bill show restituiva i campi di testo grezzi: entita non decodificate e, su ~6.500 atti, il suffisso ^^http://...#string incollato dentro al valore del literal alla fonte (es. ac19_2316, legge IA). bills list applicava gia' decodeHtml, bill show no. +1 test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(votes): guardia AIC estesa a interrogazioni/interpellanze (review Greptile PR #71) Difesa in profondita': sul grafo non esiste una sola dc:description che inizi per quei prefissi (0 su 257.751 votazioni), ma il modo di sbagliare di questa guardia e' attribuire un provvedimento a un voto che non lo riguarda, quindi l'elenco viene chiuso comunque. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 66e9400 commit 3090638

6 files changed

Lines changed: 296 additions & 14 deletions

File tree

LOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# LOG
22

3+
## 2026-07-20
4+
5+
- **fix `votes` (Camera) — i voti su emendamento con descrizione a codice secco ora si agganciano all'atto** — sulla seduta `s19_689` (14/7/2026, prima lettura della legge elettorale) l'intera giornata è priva di `ocd:rif_attoCamera` alla fonte e solo due votazioni su dodici citano `"PDL 2822-A"` nel testo: le altre dieci hanno `dc:description` a codice secco (`"EM 1.1077"`, `"SUBEM 0.1.1077.4"`) e restavano senza `bill_uri`. Tra queste il voto sull'emendamento preferenze, 188 contrari / 187 favorevoli, cioè il voto che ha deciso la vicenda: un giornalista che filtrava i voti per numero di atto lo perdeva. Aggiunto un terzo livello di fallback in cascata (`inheritBillFromSession`): quando l'**intera** seduta verte su un solo atto, quello è l'atto anche dei voti muti. La monotematicità è calcolata con una query dedicata sulla seduta completa — mai sulle righe già filtrate in memoria, dove un `--date-from` o un `LIMIT` stretto possono mostrare un solo atto mentre la seduta ne tratta quattro — e l'insieme degli atti unisce `rif_attoCamera` e i numeri citati nelle descrizioni delle altre votazioni (su `s19_689` il primo insieme è vuoto). Mozioni e risoluzioni sono escluse: sono AIC a sé stanti, ereditare l'atto della seduta le legherebbe a un provvedimento che non le riguarda. Impatto misurato: sui 1000 voti del 2026 almeno 178 righe guadagnano il collegamento all'atto (copertura 369→547). Solo il 37% delle sedute leg. 19 è monotematico, quindi il resto resta deliberatamente senza aggancio invece di rischiare attribuzioni sbagliate. Estratto l'helper `resolveActUris` condiviso coi fallback precedenti (nessun URI fabbricato: il numero base è sempre verificato via `dc:identifier`). +4 test (aggancio, non-regressione su seduta multi-atto, guardia mozioni/risoluzioni, coerenza legislatura↔atto su finestra cross-legislatura). Nuova nota wiki OKF `docs/lod-wiki/camera/votazioni-atto-collegato.md` con i conteggi del buco per legislatura e le tre famiglie di `dc:description`. Emerso dal report news-agent `2026-07-20_19-49.md`. **Residuo noto**: `--bill-code` filtra `dc:description` lato server, quindi a monte dell'ereditarietà — i voti a codice secco restano fuori da quel percorso e vanno raggiunti per intervallo di date filtrando poi su `bill_number`. Renderlo consapevole della seduta significa selezionare l'intera seduta e filtrare a valle, con il `LIMIT` che non varrebbe più sulle righe restituite: tenuto fuori deliberatamente.
6+
7+
- **fix `bill show` (Camera) — testo ripulito da entità HTML, tag e suffisso `^^xsd:string`**`label`, `title` e `description` uscivano grezzi dal grafo: entità non decodificate (`&quot;`, `&lt;em&gt;`, `&nbsp;`) e, su circa 6.500 atti su 269.816, il suffisso `^^http://www.w3.org/2001/XMLSchema#string` incollato **dentro** al valore del literal alla fonte (verificato in JSON crudo dall'endpoint, non è un artefatto del nostro parsing). Caso concreto: `ac19_2316`, la legge sull'intelligenza artificiale. `bills list` applicava già l'helper `decodeHtml`, `bill show` era rimasto indietro: stessa classe di bug chiusa per `votes` in v0.26.1. Applicato l'helper esistente ai tre campi di testo, nessun codice nuovo. +1 test. Emerso dal report news-agent `2026-07-20_19-49.md`.
8+
39
## 2026-07-19
410

511
- **v0.26.1** — release patch. Fix `votes list` (Camera): `label`/`title`/`description` arrivavano con entità HTML doppiamente codificate (es. `all&amp;#39;emergenza` invece di `all'emergenza`), perché `votes.ts` era l'unico tool a non applicare l'helper `decodeHtml` già esistente e usato da `bills.ts`, `bill-progress.ts` e altri. Emerso dal report news-agent `2026-07-19_08-11.md` (agente news-driven-cli-gap-analyzer): 6 notizie testate a spettro temporale (oggi, 2025/leg.19, 2020/leg.18), copertura confermata solida su voti/iter/interventi/profili, unico gap reale questo bug. PR #70, mergiata via squash. 43 tool invariati, 172/174 test verdi (2 fallimenti `senato-votes` per 403 dell'endpoint Senato, non collegati).

docs/lod-wiki/camera/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Endpoint SPARQL: `https://dati.camera.it/sparql`. Ontologia OCD (namespace `http
99
* [Data di un intervento in aula](interventi-data.md) - l'intervento non porta la data (`ods:modified` è il timestamp del record); il giorno reale è su `dc:date` della `ocd:discussione` che lo raggruppa (`AAAAMMGG`), verificato per Aula e commissione. Filtro data performante solo con range filter sul soggetto (legislatura obbligatoria).
1010
* [Date degli atti di sindacato ispettivo (aic)](aic-date.md) - `dc:date` è presentazione (a volte composta `pres-modifica`), `ocd:endDate` è conclusione/trattazione; la seduta NON è un link strutturato (numero solo in `dc:description`). Trappola question time.
1111
* [Iter di un atto — timeline degli stati](iter-statoIter.md) - la cronologia dell'iter è una timeline via `ocd:rif_statoIter` (uno stato per fase, `dc:date`+`dc:title`); copertura alla pari tra legislature (18 = 19). Il ramo Senato dà per design solo lo stato corrente (timeline nel feed RSS), asimmetria di ramo non di legislatura.
12+
* [Votazione → atto collegato](votazioni-atto-collegato.md) - `ocd:rif_attoCamera` manca su circa metà delle votazioni e su alcune sedute manca all'intera giornata (es. `s19_689`, legge elettorale). L'atto si ricostruisce dal testo di `dc:description` (ODG: numero centrale di `9/<atto>/<n>`) e, per i voti a codice secco (`EM 1.1077`), solo dalla monotematicità della seduta — 37% delle sedute leg. 19.
1213
* [Firmatari di decreti-legge e atti governativi](firmatari-atti-governativi.md) - sugli atti del Governo `ocd:primo_firmatario` punta a un blank node "membro di governo", non a un deputato: il nome del ministro è via `ocd:rif_persona`, il dicastero in `ocd:ruolo`. Senza seguirli il nome torna vuoto.
1314

1415
# Fonti non-LOD (HTML/PDF)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
type: Trappola
3+
title: Il link votazione → atto (`ocd:rif_attoCamera`) è largamente assente
4+
description: Metà delle votazioni Camera non ha rif_attoCamera; su alcune sedute manca all'intera giornata. L'atto va ricostruito dal testo di dc:description e, per i voti a codice secco, dalla monotematicità della seduta.
5+
resource: https://dati.camera.it/sparql
6+
tags: [camera, votazione, atto, rif_attoCamera, emendamenti, seduta]
7+
timestamp: 2026-07-20
8+
---
9+
10+
La proprietà che collega una `ocd:votazione` al provvedimento votato è `ocd:rif_attoCamera`. È l'unico link strutturato disponibile — e non è affidabile.
11+
12+
# Quanto manca
13+
14+
Votazioni prive di `rif_attoCamera` (conteggi su `dati.camera.it/sparql`, luglio 2026):
15+
16+
| Legislatura | Votazioni | Senza atto |
17+
|---|---|---|
18+
| 17 | 24.930 | ~11.100 |
19+
| 18 | 10.724 | ~7.300 |
20+
| 19 | 18.917 | ~10.000 |
21+
22+
Non è un fenomeno solo delle votazioni recenti non ancora consolidate: il buco è distribuito su tutte le legislature e su quasi tutte le sedute.
23+
24+
# Trappola: sedute intere senza il link
25+
26+
Il caso peggiore non è il voto isolato, è la **seduta intera**. La seduta `s19_689` (14/7/2026, prima lettura della legge elettorale) ha 12 votazioni e **zero** triple `rif_attoCamera`, pur vertendo tutta su un solo atto (`ac19_2822`):
27+
28+
```sparql
29+
PREFIX ocd: <http://dati.camera.it/ocd/>
30+
SELECT DISTINCT ?atto WHERE {
31+
?v ocd:rif_seduta <http://dati.camera.it/ocd/seduta.rdf/s19_689> ;
32+
ocd:rif_attoCamera ?atto
33+
}
34+
# → 0 righe
35+
```
36+
37+
Conseguenza pratica: chi cerca "i voti sull'atto 2822" col solo link strutturato non trova nulla, incluso il voto sull'emendamento preferenze (188 contrari / 187 favorevoli) che ha deciso la vicenda.
38+
39+
# Ricostruire l'atto dal testo
40+
41+
`dc:description` è la fonte di riserva, ma il suo formato non è uniforme. Tre famiglie osservate sui voti senza atto (leg. 19, proporzioni su ~20.000 righe):
42+
43+
1. **Ordini del giorno (~58%)**`"Ordine del giorno n. 9/1049/3 ZANELLA LUANA (AVS)"`, oppure la forma breve `"ODG 9/2920/46"`. Il numero dell'atto è **il segmento centrale** della numerazione `9/<atto>/<progressivo>`, con `E ABB` come suffisso tollerato. Estraibile in modo deterministico.
44+
2. **Codice secco (~19%)**`"EM 1.1077"`, `"SUBEM 0.1.1077.4"`. Nessun riferimento all'atto, né nel testo né nel grafo. Non ricostruibile dalla singola riga.
45+
3. **Mozioni e risoluzioni (~20%)**`"MOZ 1-586"`, `"Risoluzione n. 6-263"`. Sono AIC a sé stanti: **è corretto** che non abbiano un atto collegato, non vanno agganciate.
46+
47+
Nella stessa seduta le due forme convivono: `vs19_689_002` porta `"PDL 2822-A E ABB - EM 1.1"` (atto ricostruibile) mentre `vs19_689_012` porta solo `"EM 1.1077"`.
48+
49+
# Monotematicità della seduta
50+
51+
Per la famiglia 2 l'unico aggancio possibile è la seduta: se **tutta** la seduta verte su un solo atto, quello è l'atto anche dei voti muti. Distribuzione degli atti per seduta in leg. 19 (408 sedute con votazioni):
52+
53+
| Atti distinti nella seduta | Sedute |
54+
|---|---|
55+
| 1 | 149 |
56+
| 2 | 167 |
57+
| 3 | 63 |
58+
| 4+ | 29 |
59+
60+
Solo il 37% delle sedute è monotematico: l'ereditarietà va applicata **solo** a quelle, altrimenti si attribuiscono emendamenti all'atto sbagliato. Due cautele non negoziabili:
61+
62+
* la monotematicità va calcolata con una query dedicata **sull'intera seduta**, mai sulle righe già filtrate in memoria: un filtro per data o un `LIMIT` stretto possono mostrare un solo atto quando la seduta ne tratta quattro;
63+
* l'insieme degli atti della seduta va costruito dall'unione di `rif_attoCamera` **e** dei numeri citati nelle `dc:description` delle altre votazioni — sulle sedute come `s19_689` il primo insieme è vuoto.
64+
65+
# Implementazione nel progetto
66+
67+
`src/tools/votes.ts` applica i tre livelli in cascata: `rif_attoCamera` → numero citato in `dc:description` (`extractBillNumber`, copre DDL/PDL e ODG) → ereditarietà dalla seduta monotematica (`inheritBillFromSession`, esclude mozioni e risoluzioni). Il numero base è sempre risolto a URI verificandone l'esistenza via `dc:identifier`: nessun URI fabbricato.

src/tools/bill.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from "zod";
22
import { cdQuery } from "../core/client.js";
33
import { OCD_PREFIXES } from "../core/prefixes.js";
44
import { flattenBindings } from "../core/flatten.js";
5+
import { decodeHtml } from "../core/decode-html.js";
56
import { actHtmlUrl } from "../core/html-url.js";
67
import type { Tool } from "./types.js";
78

@@ -82,11 +83,14 @@ SELECT ?contributor WHERE {
8283
const rows = [
8384
{
8485
uri: input.uri,
85-
label: r.label ?? "",
86-
title: r.title ?? "",
86+
// I campi di testo arrivano dal grafo con entità HTML, tag <em> e — su
87+
// circa 6.500 atti — il suffisso "^^http://...#string" incollato dentro
88+
// al valore del literal: stessa pulizia già applicata da `bills list`.
89+
label: decodeHtml(r.label ?? ""),
90+
title: decodeHtml(r.title ?? ""),
8791
type: r.type ?? "",
8892
date: r.date ?? "",
89-
description: r.description ?? "",
93+
description: decodeHtml(r.description ?? ""),
9094
initiative: r.initiative ?? "",
9195
identifier: r.identifier ?? "",
9296
sponsor_uri: r.primo_firmatario ?? "",

src/tools/tools.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,72 @@ describe("Camera tools", () => {
148148
expect(result.rows[0]).toHaveProperty("approved");
149149
}, 30000);
150150

151+
it("votes: seduta monotematica aggancia all'atto anche i voti a codice secco (EM 1.1077, 188-187)", async () => {
152+
// Legge elettorale, 14/7/2026: l'intera seduta s19_689 è priva di
153+
// rif_attoCamera alla fonte e solo due voti citano "PDL 2822-A" nel testo.
154+
// Il voto decisivo sull'emendamento preferenze ("EM 1.1077") deve comunque
155+
// risultare agganciato all'atto, altrimenti è invisibile a chi filtra per
156+
// numero di provvedimento.
157+
const result = await votesTool.execute({
158+
dateFrom: "2026-07-14",
159+
dateTo: "2026-07-14",
160+
limit: 100,
161+
offset: 0,
162+
});
163+
const em = result.rows.find((r) => r.uri.endsWith("vs19_689_012"));
164+
expect(em?.bill_uri).toBe("http://dati.camera.it/ocd/attocamera.rdf/ac19_2822");
165+
expect(em?.bill_number).toBe("2822");
166+
}, 30000);
167+
168+
it("votes: seduta multi-atto non eredita l'atto (nessuna attribuzione ambigua)", async () => {
169+
// 25/6/2025, seduta s19_499: quattro atti distinti nella stessa giornata.
170+
// Gli agganci devono venire solo da rif_attoCamera o dal testo, mai
171+
// dall'ereditarietà di seduta — un filtro per data potrebbe mostrarne uno
172+
// solo e produrre attribuzioni sbagliate.
173+
const result = await votesTool.execute({
174+
dateFrom: "2025-06-25",
175+
dateTo: "2025-06-25",
176+
limit: 100,
177+
offset: 0,
178+
});
179+
const byBill = new Set(result.rows.map((r) => r.bill_uri).filter(Boolean));
180+
expect(byBill.size).toBeGreaterThan(1);
181+
const art1 = result.rows.find((r) => r.uri.endsWith("vs19_499_014"));
182+
expect(art1?.bill_uri).toBe("http://dati.camera.it/ocd/attocamera.rdf/ac19_1049");
183+
}, 30000);
184+
185+
it("votes: su finestra a cavallo di due legislature l'atto resta nella legislatura giusta", async () => {
186+
// Lug-dic 2022: la finestra contiene sia gli ultimi voti della 18 sia i
187+
// primi della 19. I numeri d'atto si ripetono tra legislature ("100" esiste
188+
// in entrambe): se la risoluzione numero → URI non è chiavata anche sulla
189+
// legislatura, un voto della 18 si ritrova agganciato a un atto ac19_*.
190+
const result = await votesTool.execute({
191+
dateFrom: "2022-07-01",
192+
dateTo: "2022-12-31",
193+
limit: 600,
194+
offset: 0,
195+
});
196+
const linked = result.rows.filter((r) => r.bill_uri);
197+
expect(new Set(linked.map((r) => r.legislature_uri)).size).toBe(2);
198+
for (const r of linked) {
199+
const leg = r.legislature_uri.split("_").pop();
200+
expect(r.bill_uri).toContain(`/ac${leg}_`);
201+
}
202+
}, 60000);
203+
204+
it("votes: mozioni e risoluzioni restano senza atto anche in seduta monotematica", async () => {
205+
// 30/6/2026: solo RIS/MOZ. Sono AIC a sé stanti, ereditare l'atto della
206+
// seduta le legherebbe a un provvedimento che non le riguarda.
207+
const result = await votesTool.execute({
208+
dateFrom: "2026-06-30",
209+
dateTo: "2026-06-30",
210+
limit: 100,
211+
offset: 0,
212+
});
213+
expect(result.rows.length).toBeGreaterThan(0);
214+
for (const r of result.rows) expect(r.bill_uri).toBeFalsy();
215+
}, 30000);
216+
151217
it("votes: date range uses STR() so it is not a silent empty (feb 2025 ≈ 436)", async () => {
152218
// Sentinella anti-regressione: il confronto data Camera va avvolto in STR().
153219
// Senza STR() Virtuoso fa un confronto numerico spurio → 0 righe mute pur
@@ -417,6 +483,23 @@ describe("Camera tools", () => {
417483
expect(result.rows[0]).toHaveProperty("initiative");
418484
}, 30000);
419485

486+
it("bill: ripulisce label e title da entità HTML, tag e suffisso ^^xsd (legge IA)", async () => {
487+
// ac19_2316: il literal arriva dal grafo con &quot;/&lt;em&gt;/&nbsp; e con
488+
// "^^http://www.w3.org/2001/XMLSchema#string" incollato dentro al valore.
489+
// `bills list` lo ripuliva già, `bill show` no.
490+
const result = await billTool.execute({
491+
uri: "http://dati.camera.it/ocd/attocamera.rdf/ac19_2316",
492+
});
493+
const r = result.rows[0];
494+
for (const field of ["label", "title", "description"] as const) {
495+
expect(r[field]).not.toContain("^^");
496+
expect(r[field]).not.toMatch(/&(?:quot|lt|gt|amp|nbsp|#\d+);/);
497+
expect(r[field]).not.toMatch(/<[^>]+>/);
498+
}
499+
expect(r.label).toContain('"Disposizioni e deleghe al Governo in materia di intelligenza artificiale"');
500+
expect(r.label).toContain("(approvato dal Senato)");
501+
}, 30000);
502+
420503
it("search: finds Meloni in camera", async () => {
421504
const result = await searchTool.execute({
422505
name: "meloni",

0 commit comments

Comments
 (0)