Provenance¶
Every payable rule in the knowledge base carries a provenance record: where the value comes from and how far that source backs it. The record is machine-readable, so callers can inspect which article or table produced a figure, and the engine can refuse to present an amount as reliable when no source backs it.
Payable rules¶
A payable rule is a bundled value that the payroll run reads to compute a posted amount.
| Rule | Where it lives | Record |
|---|---|---|
| Salary table | agreement/*.json: levels[].base_salary.periods[] |
Per period, or inherited from the level |
| Fixed allowance | agreement/*.json: levels[].fixed_allowances[] |
Per allowance, or inherited from the level |
| Seniority increments | agreement/*.json: parameters.seniority_increments |
Per block |
| Extra-month entitlement | agreement/*.json: parameters.additional_months.periods[] |
Per period |
| Extra-month accrual threshold | agreement/*.json: parameters.accrual_rule |
Per rule; a CCNL without the field is listed as missing (the engine default applies) |
| First-tier overtime bands | agreement/*.json: work_rules.time_supplements.overtime_bands[] with code OT_*, kind percentage, no hour threshold and no context condition |
Per band |
| Absence rule (daily quota of partial months and sick days) | agreement/*.json: work_rules.absence_rules |
Per rule |
| Sickness rule | agreement/*.json: work_rules.sickness_rules |
Per rule |
| INPS sick-pay indemnity bands | sickness/indemnity/rates.json: bands, regimes[<id>] |
Sibling bands_provenance; one provenance per regime |
| IRPEF brackets | sector/<year>/<sector>.json: irpef_brackets |
Sibling irpef_brackets_provenance |
| Art. 13 work deduction, its minimum | sector/<year>/<sector>.json: work_deduction, work_deduction.minimum |
Per block |
| Trattamento integrativo, ulteriore detrazione | sector/<year>/<sector>.json |
Per block |
| Somma esente | tax_credit/somma_esente/<year>.json: somma_esente |
Per block |
| TFR divisor, additional IVS deduction | sector/<year>/<sector>.json: tfr, tfr.additional_ivs |
Per block |
| Fixed-term addizionale NASpI | sector/<year>/<sector>.json: fixed_term_additional_rate, fixed_term_renewal_increment, fixed_term_exempt_categories |
One sibling fixed_term_additional_rate_provenance for the three |
| INPS rates, 1% additional IVS | contribution/<year>/<sector>.json: inps, inps.employee_additional, apprentice, domestic_contributions |
Per block |
| Regional and municipal surtax | surtax/regional/<year>.json, comunale-<year>.json |
Per table (file-level provenance); an entry may override it |
| Art. 12 family deductions | family/<year>.json: spouse, children, other_dependents |
Per block |
| Fringe-benefit thresholds, PdR limits | variable_pay/2026.json: fringe_benefit, pdr |
Per block |
| Substitute-tax regimes | variable_pay/2026.json: rinnovo, notte_festivi_turni |
Their source location with source_status |
CCNL rules carry one record per rule, because each salary tranche and allowance is read from its own row of a table. Fiscal values are statutory parameters stated once per block (the brackets of one comma, the constants of one article), so their record is per block: a record per bracket would repeat the same citation, and a record per file would mix blocks with different backing (for example the public administration fixed-term exemption, cited from commentary, sits next to the IRPEF brackets of the law). The surtax tables come from one MEF publication each, so they carry one record per table instead of one per municipality.
A sub-block of a fiscal block that carries its own record (the Art. 13 minimum, the TFR additional IVS, the 1% employee IVS) is a payable rule of its own, with the capabilities of its block: the inventory walks every nested record of a block, not a fixed list of keys. The per-row records of the surtax tables are the exception: the inventory keeps one rule per table, while a run reports the record of the row it read.
Bundled values the run does not read are not payable: the other CCNL work
rules (overtime bands beyond an hour threshold, conditional or paid per hour
or per shift, leave), apprenticeship tracks, the Art. 15 deductions and the
sterilizzazione_detrazioni block. That block holds the 440 EUR reduction of
art. 16-ter c. 5-bis TUIR (L. 199/2025 art. 1 c. 4), which lowers only the
19% oneri, party donations and catastrophe premiums: the payroll computes
none of them, so the reduction never touches the Art. 12 and Art. 13
deductions or the ulteriore detrazione.
The provenance record¶
{
"provenance": {
"status": "derived",
"location": {
"source_document": {
"document_id": "ccnl-commercio-confcommercio-2019",
"title": "CCNL Terziario Distribuzione e Servizi, Testo Unico 2019",
"kind": "associazione",
"url": "https://www.confcommercio.it/-/ccnl-terziario-distribuzione",
"published_on": "2024-03-22"
},
"section": "Art. 205, Scatti di anzianità",
"quote": null
},
"extraction": {
"method": "manual",
"extraction_timestamp": "2026-08-30T00:00:00",
"verified_by": null,
"verified_at": null,
"verification_status": "unverified",
"effective_from": "1990-01-01",
"effective_until": null,
"back_calculation": null
},
"transformation": null,
"note": "Dieci scatti triennali"
}
}
| Field | Description |
|---|---|
status |
verified, derived, assumed or missing (see below) |
location.source_document |
The document: document_id, title, kind, url, published_on. The document_id with published_on identifies the version |
location.section / page / quote |
Article, comma or table; page; verbatim excerpt |
extraction |
Method, validity (effective_from / effective_until) and reviewer (verified_by / verified_at). Optional: when absent, the validity is the one of the period or ruleset that holds the rule |
transformation |
How the source text became the stored value, when not verbatim |
note |
Free-form context, such as a known simplification |
Document kinds: gazzetta, cnel, inps_circolare, legge, dpr, dl,
dlgs and amministrazione (MEF, Agenzia delle Entrate) are official;
associazione, tabella_retributiva, rivista and altro are secondary.
Authority is a separate axis from the status: a derived value may come
from a secondary document.
Provenance status¶
| Status | Meaning | Record requirements |
|---|---|---|
verified |
A named reviewer, a person or an AI model, checked the value against the cited location on a recorded date | A citation (below), extraction.verified_by and extraction.verified_at; in a CCNL file, extraction.review_id |
derived |
Taken or computed from a cited document location, without a recorded check | A citation (below) |
assumed |
Adopted without a located citation: an unchecked AI extraction, a reconstruction or estimate, or a value whose clause was never located | location optional |
missing |
No source backs the value | no location |
The model rejects a record whose status disagrees with what it records.
A label never outruns its evidence¶
A derived or verified record claims a located source, so the loaders
and the schema gate reject it when:
- it has no citation: an http(s)
location.source_document.urland alocation.sectionorlocation.page(the article and comma, or the page of the table). A title without a URL, or a URL without a clause, is not a citation; - the ruleset of its file declares
source_type: "estimated": the file says its values are approximated without a primary source, so no rule in it can be read from one; - its
noteortransformationrecords an estimate.
extraction.verification_status: "unverified" is not a reason: it is what
derived means, a value read from a cited location without a recorded
check. A record that fails is assumed, keeping its location, quote and
transformation, and its note says why.
scripts/provenance/demote.py applies the rule to the whole bundle
(every provenance record of every data file, payable or not, and the
source_status of the substitute-tax regimes) and rehashes the files it
changes; it only lowers labels. A record raised back to derived after
its source is located must drop that note, which the gate reads as an
estimate.
Nothing becomes verified without a named reviewer and a date. The
reviewer is a person, named by role (maintainer), or an AI model, named by
its identifier (e.g. claude-opus-5-5) in verified_by. In a CCNL file the
record also names the entry of the
review log that records the review
(extraction.review_id). The legacy extraction.verification_status:
"verified" alone, or an entry of the review log, does not say which value
was checked by whom, so such records are derived.
A statutory block that is the same for every sector lives in a year file
of its own, with an official_primary ruleset, rather than in the
estimated sector files: the somma esente of L. 207/2024 art. 1 cc. 4-5
(tax_credit/somma_esente/<year>.json, ruleset tax/<year>/somma-esente)
is derived, its record quoting the Gazzetta Ufficiale text of the two
commi and saying in transformation how the bands store them. The loader
merges it into the rules of every sector, and a sector file that carries
its own copy does not load. The IRPEF brackets, the Art. 13 deduction and
the other credits stay in the sector files, assumed, until their text
is quoted the same way.
scripts/provenance/assign.py assigns the status of existing
records from what they record and fills the fiscal blocks from the citations
in each file's notes and ruleset source (or, where the data records none,
the rule model docstrings; the record then says so in transformation).
The script is idempotent and rehashes the files it changes.
Current counts¶
Generated from the bundle by scripts/documentation/trust_count/generate.py; CI fails
when they drift.
| Status | CCNL rules | Fiscal blocks | Total |
|---|---|---|---|
verified |
1 594 | 102 | 1 696 |
derived |
4 690 | 31 | 4 721 |
assumed |
699 | 133 | 832 |
missing |
59 | 0 | 59 |
Of the 59 missing
rules, 59 are
extra-month accrual thresholds of CCNLs whose signed clause is not in the
bundle. assumed covers every rule of a ruleset that declares
source_type: "estimated" (the 2026 sector tax and INPS files and a few
CCNLs), the
AI-extracted CCNL values (including
30 accrual
thresholds read from signed texts, each with its article and quote),
extra-month counts with no located clause, the
edilizia INPS proxy, the PA apprentice placeholder, the PA
fixed-term exemption and the regional surtax table.
python scripts/provenance/check.py --rules prints the same counts.
Enforcement¶
- Load time. A schema-0.5 CCNL without a record on a level, salary
period, allowance, seniority block or additional-months period does not
load. No CCNL, tax, INPS or surtax file loads with a label that outruns
its evidence (see above): the
loader raises
DataIntegrityError. - CI, schema gate.
scripts/provenance/check.py --schemafails when any payable rule of the bundle has no record or an unknown status, and lists everymissingrecord. It also fails when a record lacks the evidence its status or readiness claims (see below), when a label outruns its evidence, when areviewedorproductionCCNL has anassumedormissingpayable rule in its own file or aconfidenceother thanverified, or when a CCNL ruleset id is notccnl/<ccnl_id>. These errors have no baseline: the gate rejects every one.tests/knowledge/ccnl_engine/bundle/validators/test_data_quality.pyruns the same inventory. - CI, evidence gate.
scripts/provenance/check.py --evidencecompares the bundle with the shrink-only baselinescripts/provenance/baseline.json, which lists everyassumedormissingpayable rule and every open model limitation. It judges each rule, never the weakest status of a capability, and fails on a rule or a limitation the baseline does not list, on a rule weaker than its baseline status, and on a baseline entry that no longer holds. The counts per capability and per CCNL therefore never grow; the gate prints them, and the capability matrix shows them for each capability and each CCNL. - Run time. A capability that executed and read a
missingrule adds arule_source_missingissue naming the rule, and the result isincomplete.result.capability_report.rule_sourcesholds, for each executed capability, the weakest status among the rules it read; anassumedormissingone is arule_source_weakblocker and the result is not payable. See Assurance.
Evidence for promotion¶
Promoting a rule to verified, or a CCNL to readiness production, is a
human task: no script raises a status. The schema gate requires:
| Claim | Fields |
|---|---|
Rule verified |
A citation, extraction.verified_by, extraction.verified_at, an exact location (location.page or location.section), and location.source_document.sha256, the sha256 of the document file the value was read from; in a CCNL file, an extraction.review_id that names an entry of verification.reviews |
CCNL readiness reviewed |
confidence verified, an entry in verification.reviews, no assumed or missing payable rule in the CCNL file, verification.owner and verification.review_due after the last review |
CCNL readiness production |
The reviewed evidence |
Updating the evidence baseline¶
After sourcing a rule or resolving a limitation, shrink the baseline and commit it with the data change:
The command refuses to add an entry. A new assumed or missing rule (for
example a new CCNL without parameters.accrual_rule, which is listed as
missing) is accepted only with an explicit flag, and the pull request
must justify the baseline diff:
Reading provenance¶
from ccnl_engine.catalog.loaders import load_ccnl
ccnl = load_ccnl("commercio-confcommercio.json")
for level in ccnl.levels:
for period in level.base_salary.periods:
record = period.provenance or level.provenance
if record is None or record.location is None:
continue
src = record.location.source_document
print(f"{level.code}: {src.title} ({src.url})")
print(f" section: {record.location.section}")
print(f" status: {record.status}")
A PeriodResult links back to its sources through result.bundle_version,
the rulesets in result.rulesets, the rule and rule_version of each
decision in result.decisions, and the per-capability statuses in
result.capability_report.rule_sources. The full
JSON of each contract, provenance included, is shown on its page under
Contracts.