Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions LOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# LOG

## 2026-07-15

- **`senato-votes` — emptyHint dinamico, data-driven (sonda del grafo invece di buchi hardcoded)** — l'`emptyHint` era una stringa statica che su ogni risultato vuoto ripeteva tre blocchi (`--ddl-uri`, filtri label, buco COVID 10/3–16/4/2020) anche fuori contesto: cercando la fiducia Milleproroghe (26/2/2020, `--confidence-vote`) l'agente vedeva l'avviso su una finestra che non interseca il 26/2 e il testo su `--ddl-uri` mai usato. Ora, sui risultati vuoti con vincolo di data, `execute()` valorizza `result.hint` (che ha precedenza su MCP+CLI) **sondando il grafo al volo**: 2 COUNT leggeri (sedute d'Assemblea `osr:SedutaAssemblea` + votazioni) sull'intervallo distinguono lo stato reale senza sapere in anticipo dov'è il buco — `sedute=0` → nessuna seduta; `sedute>0, votazioni=0` → fingerprint di un buco della fonte (rileva dinamicamente il gap #36 su qualsiasi data); `votazioni>0` → dato pieno, è il filtro (caso 26/2: 21 votazioni ma nessuna fiducia). Senza vincolo di data restano i soli frammenti statici pertinenti. Verificato end-to-end su 5 contesti (leg 18 e 19, singola data e range). Funzione pura `buildSenatoVotesEmptyHint` estratta e testata (8 test); 170/170 suite verde, tsc pulito. Emerso dal report news-agent `2026-07-15_12-08.md` (P0).

## 2026-07-13

- **v0.25.2** — release patch che consolida il fix keyword-entità HTML (bug ad alto impatto: ricerche Camera silenziosamente vuote). Recepiti due rilievi Copilot sulla PR #64 (già mergiata): (1) `htmlEntityKeywordVariants` copriva solo le vocali accentate **minuscole** → una keyword in maiuscolo (`CRIMINALITÀ`) non generava la variante `À` e, nonostante il match SPARQL sia `LCASE`-insensitive, falliva sul dato grezzo; aggiunte À/È/É/Ì/Ò/Ù alla mappa e al regex, con test di regressione dedicato; (2) il test di regressione usava `limit: 10`, fragile rispetto all'ordinamento (19 atti matchano `criminalità`): alzato a 100. 43 tool invariati, 162/162 test verdi.
Expand Down
83 changes: 83 additions & 0 deletions src/tools/senato-votes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect } from "vitest";
import { buildSenatoVotesEmptyHint } from "./senato-votes.js";

describe("buildSenatoVotesEmptyHint", () => {
describe("con sonda (path per data)", () => {
it("sedute=0 → nessuna seduta d'Assemblea", () => {
const h = buildSenatoVotesEmptyHint(
{ dateFrom: "2020-08-15", dateTo: "2020-08-15" },
18,
{ sedute: 0, votazioni: 0 },
);
expect(h).toContain("non risulta alcuna seduta d'Assemblea");
expect(h).toContain("leg. 18");
expect(h).not.toContain("buco della fonte");
});

it("sedute>0 e votazioni=0 → fingerprint buco della fonte", () => {
const h = buildSenatoVotesEmptyHint(
{ dateFrom: "2020-03-10", dateTo: "2020-04-16" },
18,
{ sedute: 9, votazioni: 0 },
);
expect(h).toContain("9 sedute d'Assemblea ma ZERO votazioni");
expect(h).toContain("buco della fonte");
expect(h).toContain("Non inventare");
});

it("votazioni>0 → dato pieno, è il filtro (caso Milleproroghe 26/2)", () => {
const h = buildSenatoVotesEmptyHint(
{ dateFrom: "2020-02-26", dateTo: "2020-02-26", confidenceVote: true },
18,
{ sedute: 1, votazioni: 21 },
);
expect(h).toContain("21 votazioni nel LOD");
expect(h).toContain("--confidence-vote true");
expect(h).not.toContain("buco della fonte");
});

it("non menziona mai la finestra COVID hardcoded fuori contesto", () => {
// 26/2 non interseca 10/3–16/4: la vecchia stringa statica lo citava lo
// stesso. Ora nessun riferimento a date specifiche non pertinenti.
const h = buildSenatoVotesEmptyHint(
{ dateFrom: "2020-02-26", dateTo: "2020-02-26", confidenceVote: true },
18,
{ sedute: 1, votazioni: 21 },
);
expect(h).not.toContain("10 marzo");
expect(h).not.toContain("16 aprile");
expect(h).not.toContain("Cura Italia");
});

it("descrive un giorno singolo quando from===to", () => {
const h = buildSenatoVotesEmptyHint(
{ dateFrom: "2020-02-26", dateTo: "2020-02-26" },
18,
{ sedute: 0, votazioni: 0 },
);
expect(h).toContain("giorno 2020-02-26");
});
});

describe("senza sonda (frammenti statici pertinenti)", () => {
it("con --ddl-uri mostra solo il blocco DDL, non quello label", () => {
const h = buildSenatoVotesEmptyHint(
{ ddlUri: "http://dati.senato.it/ddl/58039" },
19,
);
expect(h).toContain("Con --ddl-uri");
expect(h).not.toContain("--date-from/--date-to intorno all'evento");
});

it("con filtri label mostra solo il blocco label, non quello DDL", () => {
const h = buildSenatoVotesEmptyHint({ keyword: "bilancio" }, 19);
expect(h).toContain("non compare in tutti i label");
expect(h).not.toContain("Con --ddl-uri");
});

it("senza filtri discriminanti dà indicazione generica", () => {
const h = buildSenatoVotesEmptyHint({}, 19);
expect(h).toContain("Restringi la ricerca");
});
});
});
146 changes: 144 additions & 2 deletions src/tools/senato-votes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,124 @@ import { actHtmlUrl, ddlRssUrl } from "../core/html-url.js";
import { extractBillNumber } from "../core/bill-number.js";
import type { Tool } from "./types.js";

/**
* Cosa il grafo Senato contiene DAVVERO per l'intervallo di date interrogato,
* osservato al volo quando il risultato è vuoto (non un elenco di buchi noti).
* Distingue "nessuna seduta" / "sedute senza votazioni" (buco fonte) / "dato
* pieno" senza sapere in anticipo quale data è quale.
*/
export type SenatoVotesProbe = { sedute: number; votazioni: number };

type EmptyHintInput = {
ddlUri?: string;
keyword?: string;
confidenceVote?: boolean;
finalVote?: boolean;
dateFrom?: string;
dateTo?: string;
};

function describePeriodo(dateFrom?: string, dateTo?: string): string {
if (dateFrom && dateTo)
return dateFrom === dateTo
? `giorno ${dateFrom}`
: `periodo ${dateFrom}–${dateTo}`;
if (dateFrom) return `periodo dal ${dateFrom}`;
if (dateTo) return `periodo fino al ${dateTo}`;
return "periodo indicato";
}

function describeActiveFilters(input: EmptyHintInput): string {
const f: string[] = [];
if (input.keyword) f.push(`--keyword "${input.keyword}"`);
if (input.confidenceVote !== undefined)
f.push(`--confidence-vote ${input.confidenceVote}`);
if (input.finalVote !== undefined) f.push(`--final-vote ${input.finalVote}`);
return f.join(", ");
}

/**
* Hint dinamico per risultato vuoto. Con `probe` (path per data) riporta lo
* stato reale del grafo; senza, mostra solo i frammenti statici pertinenti ai
* filtri attivi. Funzione pura: testabile senza rete.
*/
export function buildSenatoVotesEmptyHint(
input: EmptyHintInput,
effectiveLeg: number,
probe?: SenatoVotesProbe,
): string {
const parts: string[] = ["Nessuna votazione trovata."];

if (probe) {
const periodo = describePeriodo(input.dateFrom, input.dateTo);
if (probe.sedute === 0) {
parts.push(
`Nel ${periodo} non risulta alcuna seduta d'Assemblea del Senato nel LOD (leg. ${effectiveLeg}): probabilmente non si è votato in Aula in quelle date, oppure le sedute non sono state ancora caricate. Verifica le date.`,
);
} else if (probe.votazioni === 0) {
parts.push(
`Nel ${periodo} risultano ${probe.sedute} sedute d'Assemblea ma ZERO votazioni nel LOD: è il segno di un buco della fonte (le sedute esistono, le votazioni non sono state caricate). Un vuoto qui NON significa che non si sia votato: è un limite del dato, non deducibile da qui. Non inventare l'esito o i contatori del voto.`,
);
} else {
const filtri = describeActiveFilters(input);
parts.push(
`Nel ${periodo} risultano ${probe.votazioni} votazioni nel LOD, ma nessuna corrisponde ai filtri attivi${filtri ? ` (${filtri})` : ""}: la votazione cercata potrebbe avere un label diverso, oppure quella specifica votazione non è nel LOD pur essendoci le altre della seduta. Riprova allargando o togliendo i filtri e riconosci il voto dal label. Non inventare l'esito o i contatori.`,
);
}
return parts.join(" ");
}

if (input.ddlUri) {
parts.push(
"Con --ddl-uri: nel LOD non risulta alcuna votazione d'Assemblea collegata a quel DDL (fiducie incluse). Verifica l'URI del DDL, oppure il voto potrebbe essere avvenuto solo in Commissione.",
);
}
if (
input.keyword ||
input.confidenceVote !== undefined ||
input.finalVote !== undefined
) {
parts.push(
"Il tema o il tipo cercato non compare in tutti i label: riprova filtrando per data (--date-from/--date-to intorno all'evento) e riconosci il voto dal label.",
);
}
if (parts.length === 1)
parts.push(
"Restringi la ricerca per data (--date-from/--date-to) o per DDL (--ddl-uri).",
);
return parts.join(" ");
}

/**
* Sonda le date: conta sedute d'Assemblea e votazioni nell'intervallo. Eseguita
* SOLO sul path vuoto con almeno un vincolo di data (2 query leggere, range
* filter performante su Virtuoso). Usa `effectiveLeg` (derivata dal DDL se dato).
*/
async function probeSenatoDates(
effectiveLeg: number,
dateFrom?: string,
dateTo?: string,
): Promise<SenatoVotesProbe> {
const bounds: string[] = [];
if (dateFrom) bounds.push(`?d >= "${dateFrom}"^^xsd:date`);
if (dateTo) bounds.push(`?d <= "${dateTo}"^^xsd:date`);
const filter = bounds.length ? `FILTER(${bounds.join(" && ")})` : "";
const sedQ = `${OSR_PREFIXES}
SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE {
?s a osr:SedutaAssemblea ; osr:legislatura ${effectiveLeg} ; osr:dataSeduta ?d .
${filter}
}`;
const votQ = `${OSR_PREFIXES}
SELECT (COUNT(DISTINCT ?v) AS ?n) WHERE {
?v a osr:Votazione ; osr:legislatura ${effectiveLeg} ; osr:seduta ?s .
?s osr:dataSeduta ?d .
${filter}
}`;
const sedute = Number(flattenBindings(await snQuery(sedQ))[0]?.n ?? "0");
const votazioni = Number(flattenBindings(await snQuery(votQ))[0]?.n ?? "0");
return { sedute, votazioni };
}

const inputSchema = z.object({
legislature: z
.number()
Expand Down Expand Up @@ -77,8 +195,12 @@ export const senatoVotesTool: Tool<typeof inputSchema> = {
name: "senato-votes",
description:
"[SENATO] Lista votazioni dell'Assemblea del Senato con esito, contatori (favorevoli, contrari, astenuti, presenti, votanti), tipo, data seduta e DDL collegato. Filtrabile per legislatura, data, DDL, parola chiave (label + titolo del DDL collegato, fiducie incluse), voti di fiducia (--confidence-vote) e voti finali (--final-vote). Per il voto del singolo senatore usare senato-vote-detail. Le votazioni di FIDUCIA hanno ddl_uri vuoto alla fonte (il DDL è solo nel label, es. 'Disegno di legge n.1933. Votazione questione di fiducia'), ma --ddl-uri le include comunque: risolve le sedute del DDL e ricollega la fiducia votata quel giorno. Verifica sempre il ddl_uri di una 'Votazione finale' trovata per data: può appartenere a un atto diverso (testo unificato). Riporta solo i contatori restituiti, non stimarli.",
// Fallback statico: sui risultati vuoti execute() valorizza `result.hint`
// (dinamico, con sonda del grafo sul path per data), che ha precedenza. Questo
// testo resta solo come rete di sicurezza e non afferma buchi specifici: quelli
// li rileva la sonda al volo, non un elenco hardcoded.
emptyHint:
"Nessuna votazione trovata. Con --ddl-uri significa che nel LOD non risulta alcuna votazione d'Assemblea collegata a quel DDL (fiducie incluse): verifica l'URI del DDL, oppure il voto potrebbe essere solo in Commissione. Con --keyword/--confidence-vote/--final-vote: il tema/tipo non compare in tutti i label — riprova filtrando per data (--date-from/--date-to intorno all'evento) e riconosci il voto dal label. Caso limite: se cerchi una fiducia e il DDL non ha nessun altro voto d'Assemblea che lo cita, prova per data e riconosci il DDL dal label. Gap noto di dataset (legislatura 18): tra il 10 marzo e il 16 aprile 2020 (periodo COVID) non risulta ALCUNA votazione d'Assemblea nel LOD, pur essendoci le sedute con i relativi interventi — include la fiducia sul decreto Cura Italia (9/4/2020). Un vuoto in questa finestra non significa che non si sia votato: è un buco della fonte, non deducibile da qui. Non inventare l'esito o i contatori del voto.",
"Nessuna votazione trovata. Un vuoto può dipendere dai filtri (riprova per data con --date-from/--date-to e riconosci il voto dal label) o da un limite della fonte (nel LOD Senato alcune sedute esistono senza le relative votazioni). Non inventare l'esito o i contatori del voto.",
inputSchema,
examples: [
"italianparliament senato-votes list --legislature 19 --limit 50",
Expand Down Expand Up @@ -265,7 +387,11 @@ SELECT DISTINCT ?date WHERE {
if (ddlDates.length === 0) {
return input.countOnly
? { rows: [{ count: "0" }], columns: ["count"] }
: { rows: [], columns };
: {
rows: [],
columns,
hint: buildSenatoVotesEmptyHint(input, effectiveLeg),
};
}
ddlDateFilter = `FILTER(?date IN (${ddlDates
.map((d) => `"${d}"^^xsd:date`)
Expand Down Expand Up @@ -509,6 +635,22 @@ SELECT ?ddl ?f WHERE {
rss_urls_json: JSON.stringify(rssUrls),
};
});
if (rows.length === 0) {
// Sonda il grafo SOLO se la ricerca ha un vincolo di data, non è per DDL
// (col DDL il ragionamento è per provvedimento, non per intervallo) ed è la
// prima pagina: con offset>0 il vuoto può essere solo "oltre l'ultima
// pagina" (votazioni>0), e la sonda affermerebbe il falso ("N votazioni ma
// nessuna matcha i filtri"). In quel caso restano i frammenti statici.
const probe =
(input.dateFrom || input.dateTo) && !input.ddlUri && input.offset === 0
? await probeSenatoDates(effectiveLeg, input.dateFrom, input.dateTo)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
: undefined;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
return {
rows,
columns,
hint: buildSenatoVotesEmptyHint(input, effectiveLeg, probe),
};
}
return { rows, columns };
},
};
Loading