Skip to content

Commit 28a38dd

Browse files
author
superpios
committed
fix: test motore (003/002/001 + determinismo + fail-closed), CI, note deterministiche, allineamento required_fields REGOLA-003 e README
1 parent ba6e166 commit 28a38dd

5 files changed

Lines changed: 146 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version: "3.11"
16+
- name: Install dependencies
17+
run: pip install -r requirements.txt
18+
- name: Run tests
19+
run: python -m pytest tests/ -q

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Nessuna pista dimostra, suggerisce o implica illecito, spreco, frode o responsab
88
**Progetto collegato** al repository madre [DoveVannoINostriSoldi](https://github.com/Italian-Builders-Org/DoveVannoINostriSoldi) (Fase 5 della ROADMAP).
99

1010
## Cosa fa
11-
- Legge le tabelle di relazione (formato definito in `schemas/` dell'Explorer)
11+
- Legge le tabelle di relazione (CSV) esportate da `investigative-explorer-dvns` (in `data/relations/` dell'Explorer); lo schema di ogni pista in uscita è in `docs/FORMATO_PISTA.md`
1212
- Applica regole dichiarative estremamente caute (YAML)
1313
- Produce piste in JSON + Markdown con provenienza completa
1414
- È completamente deterministico (stesso input → stesso output)
@@ -32,5 +32,9 @@ python scripts/apply_rules.py --input data/input --output data/leads --rules rul
3232
| docs/FORMATO_PISTA.md | Schema obbligatorio di ogni pista |
3333
| docs/LIMITI.md | Limiti metodologici e interpretativi |
3434

35+
## Note di implementazione
36+
- **Determinismo**: `generation_date` è derivato dai dati (anno massimo nei periodi osservati), non dall'orario di esecuzione. Stesso input → stesso output, in conformità al principio 6 di `docs/REGOLE_SEGNALAZIONE.md`.
37+
- **Fail-closed**: in assenza di file o di campi obbligatori non viene emessa alcuna pista (nessun errore, nessuna inferenza).
38+
3539
## Licenza
3640
GNU Affero General Public License v3.0

rules/rules_v0.1.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ rules:
2727
condition: "same_cig_or_cup linked to >= threshold distinct subjects without explicit explanation in source"
2828
threshold: 2
2929
period: "full_dataset_coverage"
30-
required_fields: ["cig_or_cup", "subject_id"]
30+
required_fields: ["cig", "cup", "subject_id"] # il motore richiede almeno uno tra cig/cup e un subject_id (vedi scripts/apply_rules.py)
3131
note: "Questo non dimostra alcun illecito. Indica solo una concentrazione che merita verifica."
3232

3333
- id: REGOLA-004

templates/lead_template.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
> Nota: template illustrativo. Il motore (`scripts/apply_rules.py`) genera il Markdown inline; questo file documenta lo schema obbligatorio definito in `docs/FORMATO_PISTA.md`.
2+
13
# {{ title }}
24

35
**ID**: {{ id }}

tests/test_engine.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import json
2+
import sys
3+
from pathlib import Path
4+
5+
import pandas as pd
6+
import yaml
7+
8+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
9+
10+
from apply_rules import ( # noqa: E402
11+
apply_regola_001,
12+
apply_regola_002,
13+
apply_regola_003,
14+
stable_id,
15+
_derive_gen_date,
16+
WHAT_CANNOT,
17+
DISCLAIMER,
18+
)
19+
20+
21+
def _df(rows, cols):
22+
return pd.DataFrame(rows, columns=cols)
23+
24+
25+
def test_regola_001_soglia():
26+
rows = [
27+
["MARIO ROSSI", "E1", "2025", "inc", "a1"],
28+
["MARIO ROSSI", "E2", "2025", "inc", "a2"],
29+
["MARIO ROSSI", "E3", "2025", "inc", "a3"],
30+
["MARIO ROSSI", "E4", "2025", "inc", "a4"],
31+
["MARIO ROSSI", "E5", "2025", "inc", "a5"],
32+
]
33+
df = _df(rows, ["person_name", "entity_id", "year"])
34+
leads = apply_regola_001(df, 5, "same_calendar_year")
35+
assert len(leads) == 1
36+
assert leads[0]["rule_id"] == "REGOLA-001"
37+
assert leads[0]["disclaimer"] == DISCLAIMER
38+
assert leads[0]["what_cannot_be_claimed"] == WHAT_CANNOT
39+
40+
41+
def test_regola_001_sotto_soglia():
42+
rows = [
43+
["MARIO ROSSI", "E1", "2025", "inc", "a1"],
44+
["MARIO ROSSI", "E2", "2025", "inc", "a2"],
45+
["MARIO ROSSI", "E3", "2025", "inc", "a3"],
46+
["MARIO ROSSI", "E4", "2025", "inc", "a4"],
47+
]
48+
df = _df(rows, ["person_name", "entity_id", "year"])
49+
assert apply_regola_001(df, 5, "same_calendar_year") == []
50+
51+
52+
def test_regola_001_fail_closed_colonne_mancanti():
53+
df = _df([["X", "E1"]], ["person_name", "entity_id"])
54+
assert apply_regola_001(df, 5, "same_calendar_year") == []
55+
56+
57+
def test_regola_002_diretti():
58+
rows = [[f"AWARD", "ENT", f"2025-0{i}-01", "affidamento diretto", "s", f"r{i}"] for i in range(1, 9)]
59+
df = _df(rows, ["awardee", "entity_id", "award_date", "procedure_type", "source_dataset", "source_record_id"])
60+
leads = apply_regola_002(df, 8, "12_months_rolling")
61+
assert len(leads) == 1
62+
assert leads[0]["rule_id"] == "REGOLA-002"
63+
64+
65+
def test_regola_002_non_diretti():
66+
rows = [[f"AWARD", "ENT", f"2025-0{i}-01", "gara ordinaria", "s", f"r{i}"] for i in range(1, 9)]
67+
df = _df(rows, ["awardee", "entity_id", "award_date", "procedure_type", "source_dataset", "source_record_id"])
68+
assert apply_regola_002(df, 8, "12_months_rolling") == []
69+
70+
71+
def test_regola_003_cig():
72+
rows = [
73+
["CIG1", "SUB1", "s", "t1"],
74+
["CIG1", "SUB2", "s", "t2"],
75+
]
76+
df = _df(rows, ["cig", "subject_id", "source_dataset", "source_record_id"])
77+
leads = apply_regola_003(df, 2, "full_dataset_coverage")
78+
assert len(leads) == 1
79+
assert leads[0]["rule_id"] == "REGOLA-003"
80+
assert leads[0]["period"] == "intero periodo coperto dal dataset"
81+
82+
83+
def test_regola_003_fail_closed_senza_cig():
84+
df = _df([["SUB1", "s", "t1"]], ["subject_id", "source_dataset", "source_record_id"])
85+
assert apply_regola_003(df, 2, "full_dataset_coverage") == []
86+
87+
88+
def test_determinismo_id_e_data():
89+
rows = [
90+
["MARIO ROSSI", "E1", "2025", "inc", "a1"],
91+
["MARIO ROSSI", "E2", "2025", "inc", "a2"],
92+
["MARIO ROSSI", "E3", "2025", "inc", "a3"],
93+
["MARIO ROSSI", "E4", "2025", "inc", "a4"],
94+
["MARIO ROSSI", "E5", "2025", "inc", "a5"],
95+
]
96+
df = _df(rows, ["person_name", "entity_id", "year"])
97+
a = apply_regola_001(df, 5, "same_calendar_year")
98+
b = apply_regola_001(df, 5, "same_calendar_year")
99+
assert json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
100+
assert a[0]["generation_date"] == "2025-01-01"
101+
assert a[0]["id"] == stable_id("REGOLA-001", "MARIO ROSSI|2025", "2025")
102+
103+
104+
def test_derive_gen_date_senza_anno():
105+
leads = [{"period": "intero periodo coperto dal dataset"}]
106+
assert _derive_gen_date(leads) == "0000-01-01"
107+
108+
109+
def test_derive_gen_date_max_anno():
110+
leads = [{"period": "2024"}, {"period": "2025"}]
111+
assert _derive_gen_date(leads) == "2025-01-01"
112+
113+
114+
def test_regola_004_disabilitata():
115+
data = yaml.safe_load(
116+
open(Path(__file__).resolve().parent.parent / "rules" / "rules_v0.1.yaml", encoding="utf-8")
117+
)
118+
r004 = next(r for r in data["rules"] if r["id"] == "REGOLA-004")
119+
assert r004["enabled"] is False

0 commit comments

Comments
 (0)