Skip to content

Commit 3603b8a

Browse files
authored
Merge pull request #57 from ondata/fix/pr56-residual-comments
fix(bill-progress): address remaining review comments after #56
2 parents 1637e58 + ffa6e54 commit 3603b8a

3 files changed

Lines changed: 32 additions & 8 deletions

File tree

LOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 2026-07-11
44

5+
- **follow-up residuale commenti PR #56 (`bill-progress`)**: migliorato l'hint del ramo Camera su risultato vuoto (ora segnala esplicitamente che `keyword/date/limit/offset` possono escludere tutte le righe), ordinamento timeline Camera reso stabile per paginazione (`ORDER BY ?date ?st`), escape robusto dei literal SPARQL con `JSON.stringify` per `keyword` (Senato + Camera), e descrizioni `dateFrom/dateTo` chiarite sui due significati (presentazione Senato vs data stato-iter Camera).
56
- **CI PR stabilizzata su due livelli**: il check richiesto `ci` ora esegue solo build + typecheck + test deterministici (esclude `src/tools/tools.test.ts` e `src/tools/sparql.test.ts` dipendenti da endpoint live). Aggiunto job `live_integration` su PR con `continue-on-error` per mantenere visibilità sui test reali Camera/Senato senza bloccare il merge per anti-bot/instabilità esterna.
67
- **follow-up commenti review su `bill-progress`**: nel ramo Camera (`number + branch=C`) ora vengono applicati anche i filtri input (`keyword`, `dateFrom/dateTo`, `limit`, `offset`) invece di essere ignorati nel return anticipato. Aggiunto `hint` dinamico specifico quando la timeline Camera è vuota (evita messaggi fuorvianti orientati al Senato) e rese neutrali rispetto all'entrypoint le `describe()` di `number`/`branch` (niente grafia `--flag`, testo valido sia per CLI sia MCP). Inclusi test su paginazione e hint del ramo C.
78
- **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.

src/tools/bill-progress.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ import { ddlRssUrl } from "../core/html-url.js";
77
import { currentLegislature } from "../core/current-legislature.js";
88
import type { Tool } from "./types.js";
99

10+
// Costruisce un letterale-stringa SPARQL valido escapando solo i caratteri che
11+
// la grammatica STRING_LITERAL2 vieta (\, ", newline, CR). Non usare
12+
// JSON.stringify: emette \uXXXX per i caratteri di controllo, sequenza che
13+
// alcuni parser SPARQL rifiutano nel corpo del letterale (o cercano un valore
14+
// diverso), rompendo il filtro keyword invece di fare il match atteso.
15+
const SPARQL_ESC: Record<string, string> = {
16+
"\\": "\\\\",
17+
'"': '\\"',
18+
"\n": "\\n",
19+
"\r": "\\r",
20+
};
21+
const sparqlStringLiteral = (value: string): string =>
22+
`"${value.replace(/[\\"\n\r]/g, (c) => SPARQL_ESC[c] ?? c)}"`;
23+
1024
const inputSchema = z.object({
1125
ddlUri: z
1226
.string()
@@ -41,12 +55,16 @@ const inputSchema = z.object({
4155
.string()
4256
.regex(/^\d{4}-\d{2}-\d{2}$/)
4357
.optional()
44-
.describe("Data inizio presentazione (YYYY-MM-DD)"),
58+
.describe(
59+
"Data inizio filtro (YYYY-MM-DD): su Senato filtra la data presentazione; su Camera (timeline) filtra la data dello stato iter",
60+
),
4561
dateTo: z
4662
.string()
4763
.regex(/^\d{4}-\d{2}-\d{2}$/)
4864
.optional()
49-
.describe("Data fine presentazione (YYYY-MM-DD)"),
65+
.describe(
66+
"Data fine filtro (YYYY-MM-DD): su Senato filtra la data presentazione; su Camera (timeline) filtra la data dello stato iter",
67+
),
5068
legislature: z
5169
.number()
5270
.int()
@@ -92,7 +110,7 @@ export const billProgressTool: Tool<typeof inputSchema> = {
92110
],
93111
async execute(input) {
94112
const cameraEmptyHint =
95-
"Nessuno stato iter Camera trovato per l'atto richiesto. Verifica il pairing legislature+number (o l'URI) e non dedurre assenza di iter dal vuoto: senza evidenza non inventare stati, date o conclusioni.";
113+
"Nessuno stato iter Camera trovato per l'atto richiesto. Verifica il pairing legislature+number (o l'URI) e, se hai usato keyword/date/limit/offset, prova ad allargare i filtri o la paginazione. Non dedurre assenza di iter dal vuoto: senza evidenza non inventare stati, date o conclusioni.";
96114

97115
// Routing per host: un URI Camera attiva il ramo "timeline iter".
98116
const isCamera = (u?: string): u is string =>
@@ -143,8 +161,9 @@ export const billProgressTool: Tool<typeof inputSchema> = {
143161
filters.push(`FILTER(?s = <${senatoDdlUri}>)`);
144162
}
145163
if (input.keyword) {
146-
const escaped = input.keyword.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
147-
filters.push(`FILTER(CONTAINS(LCASE(STR(?titolo)), LCASE("${escaped}")))`);
164+
filters.push(
165+
`FILTER(CONTAINS(LCASE(STR(?titolo)), LCASE(${sparqlStringLiteral(input.keyword)})))`,
166+
);
148167
}
149168
if (input.number) {
150169
// numeroFase è tipizzato → confronto via STR(); il ramo (osr:ramo S/C) è
@@ -234,9 +253,8 @@ async function cameraIterTimeline(
234253
) {
235254
const filters: string[] = [];
236255
if (opts.keyword) {
237-
const escaped = opts.keyword.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
238256
filters.push(
239-
`FILTER(BOUND(?titolo) && CONTAINS(LCASE(STR(?titolo)), LCASE("${escaped}")))`,
257+
`FILTER(BOUND(?titolo) && CONTAINS(LCASE(STR(?titolo)), LCASE(${sparqlStringLiteral(opts.keyword)})))`,
240258
);
241259
}
242260
if (opts.dateFrom) {
@@ -246,6 +264,10 @@ async function cameraIterTimeline(
246264
filters.push(`FILTER(STR(?date) <= "${opts.dateTo.replace(/-/g, "")}")`);
247265
}
248266

267+
// ?st NON è proiettato: entra in SELECT DISTINCT cambierebbe la chiave di
268+
// dedup (ogni risorsa-stato è unica) e reintrodurrebbe righe visivamente
269+
// duplicate quando due ?st hanno stessi titolo/date/stato. Serve solo come
270+
// tie-breaker deterministico in ORDER BY per stabilizzare la paginazione.
249271
const query = `${OCD_PREFIXES}
250272
SELECT DISTINCT ?titolo ?date ?stato WHERE {
251273
<${uri}> ocd:rif_statoIter ?st .
@@ -254,7 +276,7 @@ SELECT DISTINCT ?titolo ?date ?stato WHERE {
254276
?st dc:title ?stato .
255277
${filters.join("\n ")}
256278
}
257-
ORDER BY ?date
279+
ORDER BY ?date ?st
258280
LIMIT ${opts.limit ?? 100}
259281
OFFSET ${opts.offset ?? 0}`;
260282

src/tools/tools.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ describe("Senato tools", () => {
600600
});
601601
expect(result.rows.length).toBe(0);
602602
expect(result.hint).toContain("iter Camera");
603+
expect(result.hint).toContain("filtri");
603604
}, 30000);
604605

605606
it("bill-signatories: returns signatories for a Senato DDL", async () => {

0 commit comments

Comments
 (0)