Rules¶
The rules layer is the versioned knowledge base that the engine reads at runtime. It contains no computation logic — only structured data loaded by the loaders and validated against Pydantic schemas.
Three kinds of rules exist:
| Kind | What it holds | Format |
|---|---|---|
| CCNL | Salary tables, seniority amounts, apprenticeship tracks, work-rule parameters | JSON per contract |
| Tax | IRPEF brackets, deductions, contribution ceilings — by year | JSON per year |
| Surtax | Regional and municipal addizionale brackets — by year | JSON per year |
How the knowledge base is structured¶
src/ccnl_engine/knowledge/
├── ccnl/
│ └── data/ ← one JSON file per contract (e.g. metalmeccanico-federmeccanica.json)
├── tax/
│ └── data/ ← one JSON file per year (e.g. 2026-industria.json)
├── surtax/
│ └── data/
│ ├── regionale-2026.json ← regional brackets, all regions
│ └── comunale-2026.json ← municipal brackets, ~7 000 comuni
└── __version__ ← dataset version string (e.g. "2026.2")
All files are bundled inside the Python package and read via
importlib.resources — no filesystem paths, no network access.
Versioning¶
The entire knowledge base carries a single dataset version (__version__,
e.g. "2026.2"). It is bumped whenever any data file changes.
Every CCNL JSON additionally carries a per-contract ruleset.version and a
ruleset.source_hash (SHA-256 of the file content). The loaders verify the
hash at load time and raise ValueError on mismatch — tampered or
accidentally edited files are caught before any computation.
from ccnl_engine.catalog.loaders import load_ccnl
ccnl = load_ccnl("metalmeccanico-federmeccanica.json")
print(ccnl.ruleset.id) # "ccnl/metalmeccanico-federmeccanica"
print(ccnl.ruleset.version) # "2026.2"
print(ccnl.ruleset.source_hash) # "4a7b..."
The ruleset identity is propagated into every Calculation so a payroll
figure can be reproduced exactly from any historical snapshot. See
Trust: Versioning.
CCNL JSON schema¶
Each CCNL file follows schema version 0.5. The top-level keys:
| Key | Type | Description |
|---|---|---|
schema_version |
string | Schema revision (e.g. "0.5") |
meta |
object | Identity: CNEL code, name, sector, signatories, sources, extraction metadata |
coverage |
object | Implementation status: gross, net, work_rules, plus notes |
parameters |
object | Contract-wide parameters: hourly divisor, additional months, seniority |
levels |
array | One object per classification level: code, base salary time-series, allowances |
apprenticeship |
array | Apprenticeship tracks (under-classification or percentage) |
ruleset |
object | Identity for integrity verification: id, version, source_hash, verification_status |
work_rules |
object | Time supplements, absence, leave, and sickness rules |
meta.sources¶
Every fact in a CCNL file must trace back to a primary source. The sources
array on meta lists all documents used during extraction:
{
"document_id": "tabelle-retributive-commercio-2024",
"title": "Tabelle retributive CCNL Terziario...",
"kind": "tabella_retributiva",
"url": "https://...",
"published_on": "2024-03-28"
}
Individual payable rules (levels, salary periods, allowances, seniority,
additional months) also carry a provenance record with a status
(verified, derived, assumed or missing) that pins the exact
source_document, section, and extraction metadata for that specific
fact. See Trust: Provenance.
coverage.notes¶
Coverage notes explain what is modelled, what is simplified, and what is
absent. Four kind values:
| Kind | Meaning |
|---|---|
source |
Records which source document was used and why |
info |
Documents a modelling decision or derivation (e.g. hourly divisor formula) |
simplification |
Marks a deliberate approximation — something real that the model gets slightly wrong |
missing |
Data the engine supports but the file lacks; names the capability it leaves partial |
simplification notes are the most important for callers: they are the
known errors. Each one states its monetary_impact on what the engine
computes from the bundle (yes, no, unknown). One that can move an
amount (yes or unknown) names its capability and declares the
limitation the engine records on the runs it concerns; a file that leaves
one unmapped does not load:
{
"kind": "simplification",
"text": "Overtime bands over 48 weekly hours are not modelled.",
"capability": "overtime",
"monetary_impact": "yes",
"limitation": {
"variant": "higher_overtime_bands",
"applies_when": {"levels": ["C1"], "effective_from": "2026-01-01"},
"remediation": "Model the bands over 48 weekly hours."
}
}
applies_when narrows the runs: trigger (run, or outside_input when
the fact has no request field), contract_types, levels,
worker_categories, run_kinds, seniority_months_from, headcount_from,
effective_from, effective_until. The limitation id is <ccnl id>/<variant>; see
Model limitations. Every
simplification appears on the contract's documentation page, under "Known
simplifications".
Tax rules¶
Tax rules are loaded by year and employer size. The loader selects the
appropriate INPS rate tier (based on num_employees) and applies the
correct IRPEF bracket schedule:
from ccnl_engine.agreement.facade import TaxSector
from ccnl_engine.sector.loaders import load_year_rules
rules = load_year_rules(2026, TaxSector.INDUSTRIA, num_employees=50)
The sector must match the CCNL's meta.tax_sector. Valid values:
terziario, industria, edilizia, credito, artigianato,
pubblica-amministrazione, lavoro-domestico, agricoltura.
Surtax rules¶
Regional and municipal surtax tables are loaded by the engine for the tax
year. The caller selects them with regione and comune_belfiore on the
PeriodFacts of the run; a component whose code is None is
skipped. See Fiscal computation.
The SurtaxRules object covers all Italian regions and ~7 000 comuni
(identified by their codice catastale).