Skip to content

Commit e88a4b5

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/bill-progress-branch-c-camera
# Conflicts: # LOG.md
2 parents 148c49d + 80378ac commit e88a4b5

14 files changed

Lines changed: 337 additions & 34 deletions

File tree

.claude/agents/news-driven-cli-gap-analyzer.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,31 @@ You are a Parliamentary Data Coverage Analyst specialized in bridging real journ
3333
5. Record for each item: which command(s) tried, whether they answered the question fully / partially / not at all, and observed quality issues (empty labels, missing filters, wrong chamber coverage, truncated data, errors).
3434
6. Verify claims on BOTH chambers when relevant before concluding a capability is missing.
3535

36+
## Limiti noti della fonte — non ri-testarli né riportarli come gap CLI
37+
Alcune assenze non dipendono dal tooling: il dato non esiste (o non è ricercabile) a monte, nel LOD di Camera/Senato. Ri-scoprirle ogni run e listarle come "debolezze" è rumore. La **fonte di verità** su cosa è verificatamente assente è `docs/lod-wiki/` (pagine "assenti verificati"); l'elenco qui sotto è una scorciatoia, il wiki prevale. Regola: **non sondarle per riscoprirle e non presentarle come scoperte nuove.** L'eccezione che vale sempre la pena scrivere (una riga) è il segnale opposto: un limite noto ora *risolto*, o un tool che prima funzionava ora *rotto*.
38+
39+
- **A. Assente alla fonte, nessuna azione CLI possibile** — non sondare, non riportare:
40+
- `sindacato-ispettivo` Senato senza oggetto/testo strutturato: interrogazioni e question time del Senato non hanno un tema ricercabile per keyword → la ricerca per argomento restituisce 0, è atteso.
41+
- roll-call nominale per voti Senato non elettronici (alzata di mano, scrutinio segreto): la scelta del singolo non è registrata a monte.
42+
- firmatario specifico di atti di Governo / organi collettivi (decreti-legge): modellato come blank node, nessun parlamentare singolo.
43+
- **B. Gestito da un tool dedicato** — usa il tool, non riportare "assenza":
44+
- emendamenti Camera → `camera-amendments` (scraping dell'app HTML; non sono nel LOD).
45+
- testo di un DDL Senato → `bill-text` (dietro WAF).
46+
- iter/timeline dettagliato del Senato → campo `rss_url` di `bill-progress` (il LOD Senato espone solo lo **stato corrente**; la cronologia delle fasi è nel feed RSS).
47+
- **C. Comportamento voluto** — non ri-litigare:
48+
- `--keyword` fa match **letterale sul titolo formale/normativo** dell'atto, non ricerca semantica: se un termine giornalistico dà 0, riprova col lessico normativo prima di dedurre qualsiasi cosa (già nella skill). Non è un bug.
49+
50+
Eccezione utile: la **latenza di ingestion** dei dati Camera (sedute/discussioni che arrivano con settimane di ritardo) è source-side, ma "esporre un timestamp di ultimo aggiornamento" è un miglioramento CLI legittimo — se lo proponi, segnalalo come **già noto/tracciato**, non come scoperta nuova.
51+
3652
## Phase 3 — Output Note
3753
Write the result to `./docs/news-agent/YYYY-MM-DD_HH-MM.md` (create the `docs/news-agent/` directory if missing; use local time, zero-padded, e.g. `2026-07-01_14-30.md`).
3854
Structure the file exactly as:
3955

4056
- Title (do NOT start the title with a number)
4157
- `## Notizie analizzate` — bullet list: summary + **date (YYYY-MM-DD) and legislature** + URL + journalist data-question, per item; keep the current / 2025 / 2020 items clearly distinguishable
4258
- `## Punti di forza` — where the CLI covered the news well, with the specific command(s) that worked; note explicitly whether **historical coverage (2025 leg.19, 2020 leg.18)** held up
43-
- `## Punti di debolezza` — coverage gaps, bugs, missing filters, chamber asymmetries, **and any degradation on the historical items** (e.g. tools that only work for the current legislature, missing older data), with evidence
44-
- `## Suggerimenti implementativi` — concrete, root-cause implementation proposals (new tool, new filter, fixed field), prioritized, mapped to the news items they unlock
59+
- `## Punti di debolezza` — coverage gaps, bugs, missing filters, chamber asymmetries, **and any degradation on the historical items** (e.g. tools that only work for the current legislature, missing older data), with evidence. **Escludi le assenze source-side già note (sezione "Limiti noti della fonte", buckets A/B/C): non elencarle qui.** Se una notizia dipende davvero da una di esse, liquidala in una riga ("limite noto della fonte, vedi wiki — non un gap CLI") e passa oltre.
60+
- `## Suggerimenti implementativi` — concrete, root-cause implementation proposals (new tool, new filter, fixed field), prioritized, mapped to the news items they unlock. Non proporre di "coprire" ciò che rientra nei buckets A/B/C (per il bucket B indica il tool già esistente).
4561
- `## Comandi eseguiti` — the exact CLI invocations run, for reproducibility
4662

4763
Formatting rules: every triple-backtick code block must be preceded by a blank line. Do not reference Claude or any assistant in the document. Keep bullets short and high-signal.

.github/copilot-instructions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ Server MCP + CLI in TypeScript per interrogare i dati aperti del Parlamento ital
2626
- **Prima i tool specifici, poi lo SPARQL generico**: il tool `sparql` è l'ultima risorsa per dati non coperti da un tool dedicato.
2727
- Prima di concludere che un dato è assente, verifica **entrambe le camere** sondando le proprietà da un'istanza nota su ciascun endpoint: il "non trovato" è spesso un gap di tooling, non del dataset.
2828

29+
## Validazione e messaggi d'errore
30+
31+
- **La validazione dell'input è del framework, non dell'handler.** Lo schema `zod` di ogni tool è l'unica sorgente di verità. A runtime lo applicano gli entrypoint: l'SDK MCP valida l'input contro lo schema (`validateToolInput``safeParseAsync`) **prima** di invocare l'handler (`src/server.ts:makeHandler`), e la CLI lo fa in `src/cli.ts:runTool`. Un input malformato non arriva mai a `execute()`: non ri-validare dentro `execute` né duplicare il parse nell'handler MCP. La formattazione compatta degli `ZodError` è centralizzata in `src/core/zod-error.ts` (usata dalla CLI; l'SDK ha il suo rendering).
32+
- **Le `describe()` dello schema devono essere neutre rispetto all'entrypoint.** Lo stesso testo è mostrato sia via MCP (parametri camelCase: `dateFrom`) sia via CLI (flag kebab-case: `--date-from`). Non hardcodare la grafia del flag CLI in una `describe()`: nomina il concetto o il campo dello schema, mai `--date-from`. La mappatura camelCase↔kebab vive in un solo posto (citty per il parse degli argomenti, `zod-error` per i nomi negli errori), non nelle stringhe di prosa.
33+
2934
## Workflow di contribuzione
3035

3136
- Il repo è sotto l'organizzazione `ondata`. Le modifiche di **codice** passano sempre da **branch + Pull Request**, mai push diretto su `main`. Eccezioni: sola documentazione, o richiesta esplicita di push diretto.

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
push:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
ci:
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 35
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: "20"
24+
cache: npm
25+
26+
- name: Install dependencies
27+
run: npm ci
28+
29+
- name: Build
30+
run: npm run build
31+
32+
- name: Type check
33+
run: NODE_OPTIONS="--max-old-space-size=4096" npx tsc --noEmit
34+
35+
- name: Test
36+
run: npm test -- --run

LOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## 2026-07-11
44

55
- **fix `bill-progress``--number --branch C` dà la timeline Camera** (#41, branch `fix/bill-progress-branch-c-camera`). Prima `--number 2617 --branch C --legislature 18` restituiva il record Senato di rimando (`osr:ramo="C"`: una riga, `phase C.2617`, senza date), inutile per chi voleva l'iter Camera. Ora `--branch C` risolve l'atto Camera `ac<leg>_<num>` e ne restituisce la **timeline completa degli stati** (via `cameraIterTimeline`, stesso path di `--uri`): per il dl Covid 2020, 8 stati datati (assegnato→…→legge). `--branch S` invariato (repertorio Senato, stato corrente). **Cambio di comportamento** del valore `--branch C` (prima quasi inutile, nessun uso documentato/testato). Framing onesto dell'asimmetria in description/help/skill/wiki (`iter-statoIter.md`): Camera = timeline stati, Senato = stato corrente (timeline nel feed RSS) — riflette la fonte, non un buco della legislatura. +1 test.
6+
- **feat `speeches` — filtro `--date-from`/`--date-to` + colonna `date`** (#37, branch `feat/speeches-date-filter`). Prima gli interventi non erano filtrabili né datati nell'output. **Camera**: la data reale non è sull'intervento (`ods:modified` è il timestamp del record) ma sulla `ocd:discussione` che lo raggruppa (`dc:date` plain `AAAAMMGG`); verificato che copre sia Aula (stenografico, `sezione=assemblea`) sia commissione (bollettino), cardinalità 1 per intervento. Il join+`FILTER` sulle date va dentro la subquery-first (il filtro precede il `LIMIT`); performante solo col range filter sul soggetto → il filtro data richiede `legislature` (0.5s con, degrada senza). **Senato**: `osr:dataSeduta` (xsd:date ISO) già disponibile, filtro con `STR(?ds)` (regge sia xsd:date sia xsd:string). Colonna `date` (YYYY-MM-DD) ora su entrambe le camere — rinominata da `session_date` per parità (nessun consumer esterno). CLI (`--date-from/--date-to`), MCP, skill (tools.md + commands.md) e wiki (`camera/interventi-data.md` nuova) allineati. +2 test (Camera e Senato), 139/139 verdi, tsc pulito.
7+
- **workflow PR quasi-autonomo configurato**: aggiunto workflow `CI` su `pull_request`/`push main` (`npm ci`, `npm run build`, `NODE_OPTIONS="--max-old-space-size=4096" npx tsc --noEmit`, `npm test -- --run`) per gate automatico su PR; lato repository abilitato `allow_auto_merge`, aggiornato ruleset `Copilot review for default branch` con `review_on_push=true`, e impostata branch protection classica su `main` con status check richiesto `ci` (strict). Obiettivo: ridurre il ciclo manuale commenta/correggi/committa nelle PR.
68

79
- **v0.24.2** — release patch. Raccoglie i fix e i miglioramenti mergiati dopo la 0.24.1 (137/137 test verdi, tsc pulito, **43 tool** invariati): (1) **fix CLI emptyHint** (#49) — l'`emptyHint` statico dei tool ora arriva anche via CLI (prima cablato solo in `formatResult`/MCP), così i vuoti "legittimi" (es. gap COVID leg.18) hanno la spiegazione anche a riga di comando; via OpenSpec + `withEmptyHint`. (2) **anti-refuso `bill_number` blindato** (#52) — test di regressione sul Piano Casa (label "DDL n. 1994" → `bill_number` 1944, dal backfill `osr:fase`) e wiki `fiducia-fallback` allineata. (3) **`senato-vote-detail` — dettaglio per tipo di voto** (#53) — verificato che il dettaglio nominativo c'è per tutte le modalità; la scelta espressa (Favorevole/Contrario/Astenuto) manca solo su `segreta` e `verifica numero legale` (solo presenze). Description del tool, wiki `votazione-tipo-semantico` e skill aggiornate; segnale `roll_call_available` derivabile da `type`, niente campo nuovo. (4) **ricetta sede question time `aic`** — la sede (Aula/commissione) è già filtrabile via `--type "immediata in assemblea"/"in commissione"` (match sul label regolare), documentata in description/esempio + skill. (5) **fix agente** news-driven-cli-gap-analyzer che si auto-delegava (#50). Nessuna nuova capacità utente → patch.
810

docs/lod-wiki/camera/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Endpoint SPARQL: `https://dati.camera.it/sparql`. Ontologia OCD (namespace `http
66

77
* [Sedute e attività delle commissioni](sedute-commissione.md) - `ocd:seduta` per organo e per data; proprietà reali (`dc:date` stringa `AAAAMMGG`, `ocd:rif_organo`, `ocd:rif_leg`) e filtro legislatura obbligatorio.
88
* [Composizione delle commissioni](composizione-commissione.md) - chi ne fa parte con ruolo e date; due path RDF da unire (`ocd:membro` + `ocd:ufficioParlamentare`), trappola bicamerale sui presidenti senatori (`ocd:rif_senatore`).
9+
* [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).
910
* [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.
1011
* [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.
1112
* [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.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Data di un intervento in aula (Camera)
2+
3+
L'intervento (`ocd:intervento`) **non porta la propria data**: l'unica proprietà temporale sull'istanza è `ods:modified`, che è il timestamp di modifica del record, non il giorno in cui l'intervento è stato pronunciato. La data reale vive sulla `ocd:discussione` che raggruppa l'intervento: la discussione lo referenzia con `ocd:rif_intervento` e porta `dc:date`.
4+
5+
```sparql
6+
PREFIX ocd: <http://dati.camera.it/ocd/>
7+
PREFIX dc: <http://purl.org/dc/elements/1.1/>
8+
SELECT ?date WHERE {
9+
?disc ocd:rif_intervento <http://dati.camera.it/ocd/intervento.rdf/in19_824755> ; dc:date ?date .
10+
}
11+
```
12+
13+
`dc:date` è una stringa `AAAAMMGG` (es. `20260617`), confrontabile lessicograficamente per un filtro a intervallo. Come per gli altri campi data della Camera (`votes`, `sessions`, `aic`), il confronto va forzato con `STR(?date)`: su Virtuoso Camera i range su `dc:date` senza `STR()` rischiano un confronto numerico spurio e vuoti muti. La copertura è verificata su **entrambe** le sedi: interventi d'Aula (documento stenografico, `dc:relation` con `sezione=assemblea`) e interventi di commissione (bollettino, `sezione=bollettini`) hanno tutti la discussione con `dc:date`, con cardinalità 1 per intervento.
14+
15+
## Filtrare per data in modo performante
16+
17+
Gli interventi Camera non hanno `ocd:rif_leg`: la legislatura è solo nel pattern URI (`in<leg>_<id>`). Un filtro data va perciò abbinato al range filter sul soggetto (`FILTER(?s >= <…in19_> && ?s < <…in19_z>)`), che àncora l'indice Virtuoso; il join sulla discussione con il `FILTER` sulle date va **dentro** la subquery che seleziona/ordina/limita i soli `?s`, così il filtro precede il `LIMIT`. Senza il vincolo di legislatura la query perde l'àncora ed è molto più lenta. Questo è esattamente ciò che fa il tool `speeches` con `--date-from`/`--date-to`.

skills/italian-parliament-cli/references/commands.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,13 @@ italianparliament vote-detail show --vote-uri <vote-uri> --format csv
112112
```
113113

114114
### `speeches list`
115-
Interventi in aula (disponibili da leg. 17).
115+
Interventi in aula, Camera e Senato (`--chamber`; dati Camera da leg. 17). Colonna `date` (YYYY-MM-DD) in output.
116116
```bash
117117
italianparliament speeches list --legislature 19
118+
italianparliament speeches list --legislature 19 --date-from 2026-06-17 --date-to 2026-06-17
119+
italianparliament speeches list --chamber senato --legislature 19 --date-from 2025-03-01 --date-to 2025-03-31
118120
```
121+
`--date-from`/`--date-to` filtrano per la **data della seduta**. Camera: la data non è sull'intervento (`ods:modified` è il timestamp del record) ma sulla `ocd:discussione` che lo raggruppa — il tool la ricava. Per la Camera il filtro data richiede `--legislature` (àncora l'indice; senza è molto più lento).
119122

120123
### `attendance show`
121124
Conteggio aggregato dei voti di un deputato su tutte le votazioni della sua legislatura (favorevole/contrario/astensione/non ha votato/ha votato in scrutinio segreto) — misura di attivismo/assenteismo.

skills/italian-parliament-mcp/references/tools.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,12 @@ Come ha votato ogni deputato in una votazione.
8484
- `voteType`: Favorevole | Contrario | Astenuto | Non ha votato
8585

8686
### `speeches`
87-
Interventi in aula Camera.
88-
- `legislature`: numero legislatura (nota: dati disponibili da leg. 17)
89-
- `deputyUri`: filtra per deputato
87+
Interventi in aula, Camera **e** Senato (`chamber`).
88+
- `chamber`: `camera` (default) o `senato`
89+
- `legislature`: numero legislatura (nota: dati Camera disponibili da leg. 17)
90+
- `deputyUri`: filtra per parlamentare
91+
- `dateFrom` / `dateTo` (YYYY-MM-DD): filtra per **data della seduta** dell'intervento. Camera: la data non è sull'intervento (`ods:modified` è il timestamp del record) ma sulla `ocd:discussione` che lo raggruppa — il tool la ricava per te. Per la Camera il filtro data richiede `legislature` (àncora l'indice; senza è molto più lento).
92+
- Colonna `date` (YYYY-MM-DD) in output per entrambe le camere.
9093

9194
### `attendance`
9295
Conteggio aggregato dei voti di un deputato su tutte le votazioni della sua legislatura (misura di attivismo/assenteismo). L'URI del deputato è già specifico di una legislatura (`.../deputato.rdf/d<ID>_<LEG>`), quindi il conteggio è già delimitato senza filtro separato.

src/cli.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { CAPABILITIES, capabilityScore } from "./core/capabilities.js";
4747
import { formatRows, type Format } from "./core/format.js";
4848
import { SparqlError } from "./core/client.js";
4949
import { ZodError } from "zod";
50+
import { formatZodError } from "./core/zod-error.js";
5051
import type { ToolResult } from "./tools/types.js";
5152
import { withEmptyHint } from "./core/empty-hint.js";
5253
import { createRequire } from "module";
@@ -84,13 +85,7 @@ async function runTool(tool: { inputSchema: { parse(i: unknown): any }; execute(
8485
parsed = tool.inputSchema.parse(input);
8586
} catch (e) {
8687
if (e instanceof ZodError) {
87-
const msgs = e.issues.map((i) => {
88-
const field = i.path.join(".") || "input";
89-
return i.code === "invalid_enum_value"
90-
? `--${field}: valore non valido "${(i as { received?: string }).received ?? ""}". Ammessi: ${i.options.join(" | ")}.`
91-
: `--${field}: ${i.message}`;
92-
});
93-
throw new Error(msgs.join("\n"));
88+
throw new Error(formatZodError(e, true));
9489
}
9590
throw e;
9691
}
@@ -644,6 +639,8 @@ const speechesList = defineCommand({
644639
type: "string",
645640
description: "Full URI of a deputy/senator",
646641
},
642+
"date-from": { type: "string", description: "Start date YYYY-MM-DD (session date)" },
643+
"date-to": { type: "string", description: "End date YYYY-MM-DD (session date)" },
647644
"count-only": {
648645
type: "boolean",
649646
description: "Return only the total count",
@@ -658,6 +655,8 @@ const speechesList = defineCommand({
658655
chamber,
659656
legislature: parseIntFlag(args.legislature as string, "legislature"),
660657
deputyUri: (args["deputy-uri"] as string) || undefined,
658+
dateFrom: (args["date-from"] as string) || undefined,
659+
dateTo: (args["date-to"] as string) || undefined,
661660
countOnly: Boolean(args["count-only"]),
662661
limit: parseIntFlag(args.limit as string, "limit") ?? 100,
663662
offset: Number(args.offset ?? 0),
@@ -1861,14 +1860,7 @@ if (argv[0] === "sparql" && argv[1] !== "query" && argv[1] !== "--help" && argv[
18611860
runMain(main).catch((err: unknown) => {
18621861
if (err instanceof ZodError) {
18631862
// Errori di validazione input: per gli enum, elenca i valori validi.
1864-
const msgs = err.issues.map((i) => {
1865-
const field = i.path.join(".") || "input";
1866-
if (i.code === "invalid_enum_value") {
1867-
return `--${field}: valore non valido. Ammessi: ${i.options.join(" | ")}.`;
1868-
}
1869-
return `--${field}: ${i.message}`;
1870-
});
1871-
process.stderr.write(`Error: ${msgs.join("\n")}\n`);
1863+
process.stderr.write(`Error: ${formatZodError(err, true)}\n`);
18721864
} else if (err instanceof SparqlError) {
18731865
process.stderr.write(
18741866
`Error: ${err.message}\nEndpoint: ${err.endpoint}\n${err.status ? `Status: ${err.status}\n` : ""}`,

0 commit comments

Comments
 (0)