Trust¶
A payroll figure is only as useful as the confidence you can place in it. This section documents every mechanism the engine uses to make its results verifiable, reproducible, and transparent about their own limits.
Three layers of correctness¶
A simulation can be correct at the software level but wrong at the data level, or correct at both levels but incomplete for your specific scenario. The engine makes all three explicit:
| Layer | Question | Measured by |
|---|---|---|
| Software | Does the engine apply its rules consistently? | 100% branch coverage, mypy strict, reference table cases, legal scenario tests |
| Source | Do the modelled rules match the current authoritative sources? | Ruleset readiness tier |
| Case | Does the user's scenario fall within the modelled scope? | is_payable, blockers and assurance, with issues and decisions |
See Correctness layers for the full breakdown and a guide to reading all three together.
Ruleset readiness¶
Before reading about result-level signals, understand the ruleset-level classification that tells you whether a CCNL is cleared for a given use context.
| Tier | Symbol | Meaning | Safe for |
|---|---|---|---|
exploratory |
π§ͺ | Extracted and traced; no human review of key values | Demo, research, prototyping |
reviewed |
π | Key L1 values human-verified against primary sources | Product simulations with explicit disclaimer |
production |
π | Full review, reference case, named owner, update policy | Operational flows |
The readiness tier of every CCNL is public: engine.list_contracts() lists it
on each ContractSummary, engine.inspect_ruleset(ccnl_id) returns the full
RulesetAssurance, and every result reports it in result.rulesets. It is
also shown in the CCNL coverage matrix. An engine
built with mode="operational" blocks payment from any CCNL that is not
production.
See Readiness for promotion criteria and the current status of each tier.
Result signals¶
Every result carries its own assurance:
PeriodResult
βββ is_payable whether the amounts can be paid as computed
βββ blockers every reason they cannot, machine-readable
βββ assurance calculation, coverage, evidence, rulesets, payability
βββ rulesets rulesets the executed rules came from
βββ issues conditions that lowered the calculation axis
βββ decisions what each capability decided, from which inputs
βββ capability_report applicable capabilities the run did not cover
βββ bundle_version knowledge-base version of the calculation
CompetenceYearResult exposes the same assurance for the whole year (each axis the
worst of its runs, rulesets and blockers listed once, payable only when every
run is), the issues of its runs (each once) and their decisions in payment
order.
1. Payability¶
result.is_payable answers "can these amounts be relied upon as they are?".
A result is payable only when nothing blocks it; each ResultBlocker has a
stable code, the feature it concerns, a machine-readable detail, a
remediation and the totals it leaves unreliable. Each total has its own
answer: result.is_net_pay_payable for the net pay of the worker and
result.is_employer_cost_complete for period_employer_cost. An amount the
CCNL owes for every worker and the engine does not compute (the mètaSalute
health fund of the Metalmeccanico CCNL) leaves the employer cost short: the
net pay can be paid, the cost cannot be reported as complete.
if not result.is_payable:
for blocker in result.blockers:
print(blocker.code.value, blocker.feature, blocker.detail)
Known simplifications of the model are part of the answer:
result.assurance.limitations lists the model limitations that apply to the
run, and an open one that can move an amount is an open_limitation blocker
(see Model limitations).
An unknown normative fact never yields a payable result: the rule it drives is not applied and an issue says why. See Assurance for the payability rules, the axes and what the bundle gives today.
2. Decisions¶
result.decisions records every decision a capability took: its
reason_code, the normalized inputs it read, the rule and rule version
applied, the normative source and the amount. A capability that ran and found
nothing due still records a decision with amount 0.
Every posted amount rests on at least one decision: the base stages (pay
chain, INPS, TFR, IRPEF) record one each, and a rate or amount the caller
supplies in place of a rule is recorded with origin caller_supplied. See
Decisions.
3. Capability report¶
result.capability_report compares what the run executed with the capability
registry of the tax year, the single source of coverage (see the
capability matrix). scope says, for
every capability, whether it is applicable to the run, not_applicable or
outside_input (the request has no field for the fact that would make it
apply). Only an applicable capability can leave a gap: unsupported (the
engine does not compute it), unresolved, partial_result or
partial_implementation. Each gap blocks payability. Its status is the
coverage axis of the assurance, and rule_sources gives the weakest
provenance status of the rules each executed capability read; see
Assurance.
Provenance¶
Every payable rule (salary tables, allowances, seniority and extra-month
entitlements, INPS rates, IRPEF brackets, deductions and credits, the TFR
divisor, surtax tables, substitute-tax regime parameters) carries a
provenance record:
- status:
verified,derived,assumedormissing - source_document and section: the document and the article, table or page
- extraction: method, validity and reviewer, when recorded
- transformation: how the source text became the stored value
A rule read by a run with status assumed or missing makes the result not
payable; missing also makes it incomplete.
See Provenance for the schema, the granularity and the
current counts.
Versioning¶
Every payroll computation records exactly which ruleset versions it used:
from ccnl_engine import engine_version
print(result.bundle_version) # knowledge-base version of the calculation
print(engine_version) # engine package version
for decision in result.decisions:
print(decision.capability, decision.rule, decision.rule_version)
Each decision names the ruleset it applied (rule, rule_version), e.g.
tax/variable-pay-rules/2026 at 2026.1. To reproduce a historical result,
pin the same ccnl-engine package version: it ships the knowledge bundle.
Quality gates¶
Every change to the codebase, including JSON data files, must pass these
gates; the CI workflow runs all of them on every pull request:
| Gate | Command | Standard |
|---|---|---|
| Tests | uv run pytest |
100% branch coverage of src/ccnl_engine |
| Lint | uv run ruff check src/ tests/ scripts/ |
Zero errors |
| Format | uv run ruff format --check src/ tests/ scripts/ |
No changes |
| Types | uv run mypy src/ tests/ and uv run mypy scripts/ --explicit-package-bases |
Zero errors, strict mode |
| Provenance schema | python scripts/provenance/check.py --schema |
Every payable rule has a record with the evidence its status claims |
| Provenance evidence | python scripts/provenance/check.py --evidence |
No new assumed or missing rule, open limitation or readiness claim; the baseline only shrinks |
| Reference cases | python scripts/provenance/check.py tests/knowledge/ccnl_engine/period/reference_case/*.json |
Every reference case has a verification status and a source |
| Contract pages | uv run python scripts/documentation/contract_page/generate.py --check |
Every page matches its CCNL data |
| Trust counts | uv run python scripts/documentation/trust_count/generate.py --check |
Every count in docs/trust/ matches the bundle |
Other workflows check the capability matrix, the contracts index, the
contract examples and the cognitive complexity of src/.
JSON changes in knowledge/ (listed in knowledge/manifest.json) are treated as code-level changes: they
alter engine behaviour and carry the same gates as Python source.
No feature without provenance and reference cases¶
This is a hard rule:
Every new feature must ship with a
provenanceentry on the relevant JSON rule and tests whose expected values come from a source independent of the engine: a signed table, an official worked example or a hand calculation formulated differently.
Expected values copied from engine output detect regressions only, never a
systematic error, so they are not accepted as proof of correctness.
Reference cases live in tests/knowledge/ccnl_engine/period/reference_case/ and
tests/integration/ccnl_engine/api/test_reference_cases.py runs each one through
PayrollEngine. They are reference table cases, not full payslips: a case
asserts only the values its source states (base salary, fixed allowances and
period gross from the cited table), to the cent, and never net pay,
contributions, taxes or employer cost. Of the
35 cases,
3
are verified and
32
are source_linked. Full-payslip oracles, computed by hand from primary
sources for the whole payslip, are separate fixtures; see
Correctness.
Reference case verification status¶
Reference cases declare how far their expected values can be trusted with a
top-level verification field:
| Value | Meaning | source |
|---|---|---|
verified |
Expected values checked against an independent source (a real payslip or an official worked example) | Required, with kind, an immutable reference, verified_by, verified_at and independence |
source_linked |
The case cites the primary source it models; expected values are not checked against a payslip | Required |
Every case lists in axes the complete outputs it validates
(base_salary, gross, employee_contributions, employer_contributions,
tax, net, tfr, employer_cost) and in elements the components of a
total (fixed_allowances, contribution_base, taxable_income,
gross_irpef, tax_deductions, tax_credits, tax_surcharges) or the
rates (hourly_percentages, employee_inps_rate, employer_inps_rate) it
pins. Each expected value belongs to a declared axis or element, and each
declared one is backed by an expected value: fixed allowances alone never
claim the gross axis, an INPS rate never the contributions. A verified case names in source the
kind of output (observed_payslip or official_worked_example), an
immutable reference to it (the sha256 of the document or the path of a
file versioned in the repository), who checked it and when (verified_by,
verified_at), how its expected values were produced without the engine
(independence), and the tolerance its amounts are compared with. A case
read from the signed table the bundle models is source_linked, never
verified: it checks the transcription, not the engine.
tests/knowledge/ccnl_engine/bundle/validators/test_data_quality.py rejects a missing or unknown
value, a case without a source object, and a case whose inputs or expected
values differ from what the runner executes. In CI,
scripts/provenance/check.py validates every case, prints the count per
status, and rejects a modified case that drops its source; the same script
fails when a payable rule of the bundle has no provenance record. Expected values
are never regenerated from the engine by a script: changing one is a reviewed
edit to the JSON file.
Data operations¶
See Data operations for:
- update targets after CCNL renewals and statutory rate changes
- release notes format and economic diff per release
- error reporting process
- deprecation and version compatibility guarantees
Coverage notes and simplifications¶
Each contract's documentation page lists its coverage.notes β the
audit trail of modelling decisions. Notes marked simplification are the
known errors: places where the model intentionally diverges from the literal
contract text. Reading them before using a result in a sensitive context is
strongly recommended.
Example from CCNL Legno Arredamento Federlegno:
Known simplification
The third apprenticeship period pays an intermediate pay between the
current and the destination level. The CCNL text listed in the sources
is no longer reachable, so whether the intermediate pay covers
contingenza and EDR or the minimum table only is unverified. The engine
averages the whole monthly pay and records the open limitation
legno-arredamento-federlegno/apprenticeship_midpoint_components, so the
run is not payable.