Skip to content

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, assumed or missing
  • 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 provenance entry 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.