Skip to content

Commit 01517b7

Browse files
aborrusoclaude
andauthored
fix(committees): al Senato mancavano le Giunte e 16 organi attivi su 27 (#83)
* fix(committees): al Senato mancavano le Giunte e 16 organi attivi su 27 (#82) `--chamber senato --legislature 19` restituiva 12 organi su 27 con sedute, più nessuna delle 4 Giunte. Due cause opposte nella stessa query: - il filtro per legislatura passava dalle sedute, e le Giunte nel LOD non ne hanno nessuna: ora si aggiungono gli organi con osr:denominazione valida nel periodo della legislatura (intervallo ricavato dal grafo con MIN/MAX di osr:dataSeduta), stesso pattern temporale già usato per i gruppi; - osr:titoloBreve era un pattern obbligatorio, così i 16 URI commissione/2-* referenziati dalle sedute ma mai descritti nel grafo sparivano in silenzio, incluso quello con 647 sedute, secondo organo più attivo della legislatura. Ora è OPTIONAL: compaiono con uri e conteggio, campi di testo vuoti. I nomi mancanti restano vuoti: sono un buco della fonte, non qualcosa da riempire a valle. Il fenomeno è documentato in docs/lod-wiki e va segnalato al gestore del dato. Closes #82 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(committees): applica --limit dopo il merge, non dentro le query Da review PR #83: la query sugli organi descritti aveva LIMIT senza ORDER BY, quindi con un limite stretto il troncamento era arbitrario e poteva far sparire proprio le Giunte. Ora entrambe le query hanno un ordinamento deterministico e un tetto interno fisso; il limite dell'utente si applica alla classifica finale. Con il default il difetto costava già 2 organi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8d7865e commit 01517b7

4 files changed

Lines changed: 214 additions & 16 deletions

File tree

docs/lod-wiki/senato/sedute-commissione.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ FILTER( ?date >= "2026-05-01"^^xsd:date && ?date <= "2026-05-31"^^xsd:date )
6767
| **Doppia etichetta della commissione** | Una `Commissione` può avere **più `osr:titoloBreve`** (es. `commissione/0-2` → "Giustizia" **e** "Giustizia e autorizzazioni a procedere"). Non mettere `?comm`/`?tb` nel `SELECT` senza aggregazione, altrimenti ogni seduta si duplica. Usare `MIN(?tb)` + `GROUP BY` senza `?comm`. |
6868
| **`SAMPLE` non supportato** | Virtuoso Senato rifiuta `SAMPLE` (400). `MIN`/`MAX`/`COUNT` funzionano. |
6969
| **Niente `BIND`** | Come da [[trappole]] generali del Senato. |
70+
| **`osr:commissione` può puntare a URI mai descritti** | Le sedute referenziano organi `commissione/2-*` che **non esistono come risorse**: zero triple come soggetto, nessuna `osr:Commissione` con quel prefisso (0 su 279). Non hanno titolo né categoria, e il nome non è ricavabile da nessuna parte del grafo — nemmeno dalle sedute, che espongono solo data/tipo/legislatura. Nella sola leg. 19 sono **16 organi**, incluso `commissione/2-1` con **647 sedute**, il secondo più attivo. Conseguenza pratica: se `osr:titoloBreve` è un pattern obbligatorio nella query, questi organi **spariscono in silenzio** e si perde metà dell'attività di commissione. Tenerlo `OPTIONAL` e accettare righe senza nome. (Verificato 2026-07-26.) |
71+
| **Filtrare per legislatura via sedute esclude le Giunte** | Gli organi Senato non hanno una proprietà "legislatura": il filtro naturale passa da `?seduta osr:legislatura N`. Ma le **Giunte non hanno alcuna `SedutaCommissione`** (vedi *Assenti*), quindi con quel criterio scompaiono pur essendo attive. Alternativa: `osr:denominazione` è un nodo con `osr:inizio`/`osr:fine` (stesso pattern temporale dei gruppi), e l'intervallo della legislatura si ricava dal grafo stesso con `MIN`/`MAX` di `osr:dataSeduta` sulle sedute di quella legislatura. I due criteri vanno **uniti**: nessuno dei due basta. (Verificato 2026-07-26.) |
7072

7173
# Ricerca commissione per nome
7274

@@ -183,6 +185,8 @@ Un tool "audizioni Senato" è **fattibile via scraping HTML** (non SPARQL, non J
183185

184186
# Assenti
185187

188+
* **Sedute delle Giunte**: nessuna delle 4 Giunte del Senato (`commissione/0-20` Regolamento, `0-21` Elezioni e immunità parlamentari, `0-22` Biblioteca e archivio storico, `0-24` Verifica dei poteri) ha `osr:SedutaCommissione` collegate, in nessuna legislatura. L'anagrafica c'è ed è completa (`0-21`: 11 triple, `categoriaCommissione` = `Giunte`, sottotitolo "(Art. 19 del Regolamento)", tre `osr:denominazione` datate dal 1948), ma dell'attività non c'è traccia. È un'assenza rilevante: la Giunta delle elezioni e delle immunità è la sede delle autorizzazioni a procedere, cioè esattamente ciò che si cerca quando una vicenda giudiziaria tocca un parlamentare. (Verificato 2026-07-26.)
189+
186190
* **Commissioni bicamerali con sedute/interventi esposti**: la *Commissione parlamentare di inchiesta sul femminicidio* esiste come entità Senato (`commissione/0-141` storica leg. XVII–XVIII e `commissione/4-223` attuale XIX) con i suoi membri senatori (**24 afferenze** nella XIX), ma **non ha `SedutaCommissione`/`Intervento` collegati** nel LOD Senato (verificato 2026-07-01: 0 righe su entrambi gli URI per leg. 19). Le sedute/interventi sono pubblicati solo dalla Camera (`ocd:organo` `o19_3941`, 181 URI seduta / 157 date distinte fino a giu 2026). Vedi [[../camera/sedute-commissione]]. La composizione Senatori si ottiene con `committee-members list --committee-uri <commissione/4-223> --chamber senato`.
187191

188192
# Citations
@@ -192,3 +196,5 @@ Un tool "audizioni Senato" è **fattibile via scraping HTML** (non SPARQL, non J
192196
[3] Verifica assenza attività femminicidio Senato (2026-07-01): `committee-sessions`/SPARQL su `commissione/0-141` e `4-223` → 0 sedute, 0 interventi.
193197
[4] Risposta Webmaster Senato (email 2026-07-06): conferma che lo SPARQL non espone resoconto/OdG per `SedutaCommissione`; indica le liste JSON `listasommcomm` come fonte con link ai resoconti.
194198
[5] Schema JSON verificato via agent-browser (2026-07-06): `https://www.senato.it/static/bgt/listasommcomm/0/1/t/19/2026/index.json`; `curl` → HTTP 202 vuoto (WAF). Pattern link resoconto ricavato dai `href` della pagina resoconti: `show-doc?leg=19&tipodoc=SommComm&id=<id_testo>&idoggetto=0`.
199+
[6] URI di commissione orfani (2026-07-26): `SELECT (COUNT(*) AS ?n) WHERE { <http://dati.senato.it/commissione/2-1> ?p ?o }``0`; `SELECT (COUNT(DISTINCT ?c) AS ?n) WHERE { ?c a osr:Commissione . FILTER(STRSTARTS(STR(?c), "http://dati.senato.it/commissione/2-")) }``0`, contro 279 `osr:Commissione` totali. Organi con sedute in leg. 19: 27, di cui 16 senza `osr:titoloBreve` (tutti `2-*`).
200+
[7] Assenza sedute delle Giunte (2026-07-26): enumerazione di tutte le commissioni con `SedutaCommissione` in leg. 19 → nessuna delle 4 Giunte. Intervallo leg. 19 da `MIN`/`MAX` di `osr:dataSeduta`: `2022-10-25``2026-07-23`; commissioni con `osr:denominazione` valida in quel periodo: 59.

src/tools/committees.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, it, expect } from "vitest";
2+
import { mergeSenatoCommittees } from "./committees.js";
3+
4+
const row = (o: Partial<Record<string, string>> & { uri: string }) => ({
5+
chamber: "senato",
6+
uri: o.uri,
7+
title: o.title ?? "",
8+
short_title: o.short_title ?? "",
9+
subtitle: o.subtitle ?? "",
10+
category: o.category ?? "",
11+
session_count: o.session_count ?? "",
12+
});
13+
14+
// I due insiemi si intersecano solo in parte (#82): le Giunte hanno anagrafica
15+
// ma non sedute, gli organi "2-*" hanno sedute ma non esistono come risorse.
16+
describe("mergeSenatoCommittees", () => {
17+
it("tiene gli organi con sedute e ne completa l'anagrafica", () => {
18+
const out = mergeSenatoCommittees(
19+
[row({ uri: "c/0-1", short_title: "Affari Costituzionali", session_count: "742" })],
20+
[row({ uri: "c/0-1", title: "1a Commissione", short_title: "Affari Costituzionali", category: "Commissioni permanenti" })],
21+
);
22+
expect(out).toHaveLength(1);
23+
expect(out[0].session_count).toBe("742");
24+
expect(out[0].title).toBe("1a Commissione");
25+
expect(out[0].category).toBe("Commissioni permanenti");
26+
});
27+
28+
it("aggiunge gli organi descritti ma senza sedute, con conteggio 0 (le Giunte)", () => {
29+
const out = mergeSenatoCommittees(
30+
[row({ uri: "c/0-1", short_title: "Affari Costituzionali", session_count: "742" })],
31+
[row({ uri: "c/0-21", short_title: "Giunta delle elezioni", category: "Giunte" })],
32+
);
33+
expect(out.map((r) => r.uri)).toEqual(["c/0-1", "c/0-21"]);
34+
expect(out[1].session_count).toBe("0");
35+
});
36+
37+
it("tiene gli organi con sedute ma privi di anagrafica, senza inventarne il nome", () => {
38+
const out = mergeSenatoCommittees([row({ uri: "c/2-1", session_count: "647" })], []);
39+
expect(out).toHaveLength(1);
40+
expect(out[0].short_title).toBe("");
41+
expect(out[0].title).toBe("");
42+
expect(out[0].session_count).toBe("647");
43+
});
44+
45+
it("ordina per numero di sedute decrescente, poi per titolo", () => {
46+
const out = mergeSenatoCommittees(
47+
[
48+
row({ uri: "c/0-5", short_title: "Bilancio", session_count: "592" }),
49+
row({ uri: "c/2-1", session_count: "647" }),
50+
],
51+
[
52+
row({ uri: "c/0-22", short_title: "Biblioteca", category: "Giunte" }),
53+
row({ uri: "c/0-21", short_title: "Giunta delle elezioni", category: "Giunte" }),
54+
],
55+
);
56+
expect(out.map((r) => r.uri)).toEqual(["c/2-1", "c/0-5", "c/0-22", "c/0-21"]);
57+
});
58+
59+
it("non duplica un organo presente in entrambi gli insiemi", () => {
60+
const out = mergeSenatoCommittees(
61+
[row({ uri: "c/0-1", session_count: "742" })],
62+
[row({ uri: "c/0-1", short_title: "Affari Costituzionali" })],
63+
);
64+
expect(out).toHaveLength(1);
65+
expect(out[0].short_title).toBe("Affari Costituzionali");
66+
});
67+
});

src/tools/committees.ts

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const inputSchema = z.object({
1818
.positive()
1919
.optional()
2020
.describe(
21-
"Numero legislatura. Camera: le commissioni sono istanze per-legislatura, default 19. Senato: opzionale, senza filtro mostra tutte le commissioni storiche (nessun session_count); con filtro mostra solo quelle attive con numero di sedute.",
21+
"Numero legislatura. Camera: le commissioni sono istanze per-legislatura, default 19. Senato: opzionale, senza filtro mostra tutte le commissioni storiche (nessun session_count); con filtro mostra gli organi attivi in quella legislatura, con il numero di sedute (0 per quelli che nel LOD non ne hanno, come le Giunte).",
2222
),
2323
limit: z.number().int().min(1).max(1000).default(300),
2424
});
@@ -73,25 +73,122 @@ LIMIT ${limit}`;
7373

7474
// --- Senato ---------------------------------------------------------------
7575

76+
// Mappatura comune alle query Senato: i nomi di variabile coincidono, cambia
77+
// solo quali sono valorizzate (le sedute danno `n_sedute`, l'anagrafica i
78+
// titoli). I campi assenti restano stringa vuota.
79+
function toRows(raw: Record<string, string | undefined>[]): Row[] {
80+
return raw.map((r) => ({
81+
chamber: "senato",
82+
uri: r.comm ?? "",
83+
title: r.titolo ?? "",
84+
short_title: r.titoloBreve ?? "",
85+
subtitle: r.sottotitolo ?? "",
86+
category: r.cat ?? r.categoria ?? "",
87+
session_count: r.n_sedute ?? "",
88+
}));
89+
}
90+
91+
// Intervallo di date coperto da una legislatura, ricavato dalle sedute di
92+
// commissione: il grafo Senato non espone inizio/fine legislatura, ma le date
93+
// delle sedute la delimitano con precisione sufficiente a filtrare gli organi.
94+
async function senatoLegislatureRange(
95+
legislature: number,
96+
): Promise<{ from: string; to: string } | null> {
97+
const query = `${OSR_PREFIXES}
98+
SELECT (MIN(?d) AS ?dal) (MAX(?d) AS ?al)
99+
WHERE {
100+
?s a osr:SedutaCommissione .
101+
?s osr:legislatura ${legislature} .
102+
?s osr:dataSeduta ?d
103+
}`;
104+
const raw = flattenBindings(await snQuery(query));
105+
const from = raw[0]?.dal;
106+
const to = raw[0]?.al;
107+
return from && to ? { from, to } : null;
108+
}
109+
110+
/**
111+
* Unisce gli organi con sedute nella legislatura e quelli descritti come
112+
* attivi nel suo periodo. Sono due insiemi che si intersecano solo in parte,
113+
* e nessuno dei due basta da solo (#82): le Giunte non hanno sedute nel LOD,
114+
* e 16 organi con sedute — incluso il secondo più attivo della leg. 19 — non
115+
* esistono come risorse, quindi non hanno né titolo né categoria.
116+
* I campi vuoti restano vuoti: il nome mancante è un buco della fonte, non
117+
* qualcosa da riempire a valle.
118+
*/
119+
export function mergeSenatoCommittees(withSessions: Row[], described: Row[]): Row[] {
120+
const byUri = new Map<string, Row>();
121+
for (const r of withSessions) byUri.set(r.uri, { ...r });
122+
for (const d of described) {
123+
const seen = byUri.get(d.uri);
124+
if (!seen) {
125+
byUri.set(d.uri, { ...d, session_count: "0" });
126+
continue;
127+
}
128+
// L'anagrafica della risorsa è più ricca del solo titoloBreve aggregato
129+
// sulle sedute: completa i campi mancanti senza toccare il conteggio.
130+
seen.title ||= d.title;
131+
seen.short_title ||= d.short_title;
132+
seen.subtitle ||= d.subtitle;
133+
seen.category ||= d.category;
134+
}
135+
return [...byUri.values()].sort(
136+
(a, b) =>
137+
Number(b.session_count || 0) - Number(a.session_count || 0) ||
138+
a.short_title.localeCompare(b.short_title),
139+
);
140+
}
141+
76142
async function querySenato(
77143
legislature: number | undefined,
78144
limit: number,
79145
): Promise<Row[]> {
80146
let query: string;
81147
if (legislature) {
82-
// Filtrate per legislatura via SedutaCommissione, con conteggio sedute.
148+
// Le due query lavorano su insiemi piccoli e noti (nella leg. 19: 27 organi
149+
// con sedute, 59 descritti attivi), quindi il `limit` dell'utente si applica
150+
// DOPO il merge: troncare prima toglierebbe righe che l'ordinamento finale
151+
// avrebbe messo in testa. Il tetto interno esiste solo come guardia.
152+
const cap = 1000;
153+
// Organi con sedute nella legislatura. `titoloBreve` è OPTIONAL: quando
154+
// manca l'organo va mostrato lo stesso, altrimenti sparisce in silenzio
155+
// proprio chi ha più sedute (#82).
83156
query = `${OSR_PREFIXES}
84157
SELECT ?comm (MIN(?titoloBreve) AS ?titoloBreve) (MIN(?cat) AS ?categoria) (COUNT(DISTINCT ?seduta) AS ?n_sedute)
85158
WHERE {
86159
?seduta a osr:SedutaCommissione .
87160
?seduta osr:commissione ?comm .
88161
?seduta osr:legislatura ${legislature} .
89-
?comm osr:titoloBreve ?titoloBreve .
162+
OPTIONAL { ?comm osr:titoloBreve ?titoloBreve }
90163
OPTIONAL { ?comm osr:categoriaCommissione ?cat }
91164
}
92165
GROUP BY ?comm
93166
ORDER BY DESC(?n_sedute)
94-
LIMIT ${limit}`;
167+
LIMIT ${cap}`;
168+
const withSessions = toRows(flattenBindings(await snQuery(query)));
169+
const range = await senatoLegislatureRange(legislature);
170+
if (!range) return withSessions;
171+
// Organi la cui denominazione è valida nel periodo della legislatura:
172+
// stesso pattern temporale dei gruppi (osr:denominazione con inizio/fine),
173+
// ed è l'unico modo per vedere le Giunte, che sedute nel LOD non ne hanno.
174+
const activeQuery = `${OSR_PREFIXES}
175+
SELECT DISTINCT ?comm ?titolo ?titoloBreve ?sottotitolo ?cat
176+
WHERE {
177+
?comm a osr:Commissione .
178+
?comm osr:denominazione ?den .
179+
?den osr:inizio ?ini .
180+
OPTIONAL { ?den osr:fine ?fine }
181+
OPTIONAL { ?comm osr:titolo ?titolo }
182+
OPTIONAL { ?comm osr:titoloBreve ?titoloBreve }
183+
OPTIONAL { ?comm osr:sottotitolo ?sottotitolo }
184+
OPTIONAL { ?comm osr:categoriaCommissione ?cat }
185+
FILTER(str(?ini) <= "${range.to}")
186+
FILTER(!BOUND(?fine) || str(?fine) >= "${range.from}")
187+
}
188+
ORDER BY ?comm
189+
LIMIT ${cap}`;
190+
const described = toRows(flattenBindings(await snQuery(activeQuery)));
191+
return mergeSenatoCommittees(withSessions, described).slice(0, limit);
95192
} else {
96193
// Tutte le commissioni storiche, nessun conteggio sedute.
97194
query = `${OSR_PREFIXES}
@@ -106,25 +203,16 @@ WHERE {
106203
ORDER BY ?titoloBreve
107204
LIMIT ${limit}`;
108205
}
109-
const results = await snQuery(query);
110-
const raw = flattenBindings(results);
111-
return raw.map((r) => ({
112-
chamber: "senato",
113-
uri: r.comm ?? "",
114-
title: r.titolo ?? "",
115-
short_title: r.titoloBreve ?? "",
116-
subtitle: r.sottotitolo ?? "",
117-
category: r.cat ?? r.categoria ?? "",
118-
session_count: r.n_sedute ?? "",
119-
}));
206+
return toRows(flattenBindings(await snQuery(query)));
120207
}
121208

122209
export const committeesTool: Tool<typeof inputSchema> = {
123210
name: "committees",
124211
description:
125212
"[CAMERA+SENATO] Commissioni parlamentari (permanenti, speciali, d'inchiesta monocamerali e bicamerali, giunte, comitati). " +
126213
"Camera: filtrata per legislatura (default 19; le commissioni sono istanze per-legislatura), con dc:type come categoria. " +
127-
"Senato: senza --legislature mostra tutte le commissioni storiche (nessun session_count); con --legislature filtra le attive e aggiunge il numero di sedute.",
214+
"Senato: senza --legislature mostra tutte le commissioni storiche (nessun session_count); con --legislature unisce due insiemi — gli organi con sedute in quella legislatura (con il conteggio) e quelli la cui denominazione risulta valida nel periodo (session_count 0). Serve entrambi perché le Giunte non hanno sedute nel LOD e comparirebbero solo nel secondo. " +
215+
"ATTENZIONE (limite della fonte, non del tool): alcuni organi del Senato con molte sedute — nella leg. 19 sedici, incluso il secondo più attivo — sono referenziati con URI 'commissione/2-*' che nel grafo non esistono come risorse: niente titolo, niente categoria. Le loro righe hanno i campi di testo VUOTI e solo uri + session_count. Non dedurre il nome dell'organo dall'URI o dal numero di sedute: non è ricavabile dal dato.",
128216
inputSchema,
129217
examples: [
130218
"italianparliament committees list --chamber camera --legislature 19",

src/tools/tools.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,43 @@ describe("Senato tools", () => {
633633
expect(Number(affari!.session_count)).toBeGreaterThan(100);
634634
}, 30000);
635635

636+
it("committees (Senato): le Giunte compaiono anche se nel LOD non hanno sedute (#82)", async () => {
637+
const result = await committeesTool.execute({ chamber: "senato", legislature: 19, limit: 300 });
638+
const giunte = result.rows.filter((r) => r.category === "Giunte");
639+
expect(giunte.length).toBeGreaterThanOrEqual(2);
640+
// La Giunta elezioni e immunità è l'organo delle autorizzazioni a procedere:
641+
// prima del fix era invisibile con --legislature, perché il filtro passava
642+
// dalle sedute e per le Giunte il LOD non ne espone nessuna.
643+
const immunita = giunte.find((r) => r.short_title.includes("Giunta delle elezioni"));
644+
expect(immunita).toBeDefined();
645+
expect(immunita!.session_count).toBe("0");
646+
}, 30000);
647+
648+
it("committees (Senato): gli organi privi di anagrafica nella fonte restano visibili, senza nome inventato (#82)", async () => {
649+
const result = await committeesTool.execute({ chamber: "senato", legislature: 19, limit: 300 });
650+
// URI referenziati dalle sedute ma inesistenti come risorse (commissione/2-*):
651+
// devono comparire con le sedute e i campi di testo vuoti.
652+
const orfani = result.rows.filter((r) => r.short_title === "" && Number(r.session_count) > 0);
653+
expect(orfani.length).toBeGreaterThan(0);
654+
expect(Math.max(...orfani.map((r) => Number(r.session_count)))).toBeGreaterThan(100);
655+
for (const r of orfani) {
656+
expect(r.title).toBe("");
657+
expect(r.category).toBe("");
658+
expect(r.uri).not.toBe("");
659+
}
660+
}, 30000);
661+
662+
it("committees (Senato): --limit tronca dopo il merge, non dentro le query (#82)", async () => {
663+
// Il limite deve tagliare la classifica finale — i più attivi in testa — e
664+
// non le due query intermedie, dove taglierebbe righe che l'ordinamento
665+
// avrebbe promosso.
666+
const result = await committeesTool.execute({ chamber: "senato", legislature: 19, limit: 5 });
667+
expect(result.rows.length).toBe(5);
668+
const counts = result.rows.map((r) => Number(r.session_count));
669+
expect(counts).toEqual([...counts].sort((a, b) => b - a));
670+
expect(counts[0]).toBeGreaterThan(500);
671+
}, 30000);
672+
636673
it("committees: returns Camera committees for leg 19", async () => {
637674
const result = await committeesTool.execute({ chamber: "camera", legislature: 19, limit: 300 });
638675
expect(result.rows.length).toBeGreaterThan(10);

0 commit comments

Comments
 (0)