Assurance¶
A payroll result answers one question for an integration: can this amount be
paid as it is? The answer is result.is_payable, and result.blockers says
why not. Both come from result.assurance, a ResultAssurance derived from
the run; neither is set by the caller.
The answer is fail-closed. A run is payable only when every capability it requires is computed on facts the caller supplied, or ruled out by a decision of the run. A default never decides a capability: a fact left to its default is not the fact it stands for.
| Field | Answers |
|---|---|
result.is_payable |
Can the amounts of this result be paid as computed? |
result.blockers |
What stops them, one ResultBlocker per reason |
result.assurance |
The axes the answer is derived from: calculation, coverage, evidence, rulesets, mode, limitations |
result.rulesets |
Which rulesets (CCNL, tax, INPS, variable pay, surtax) the run read, each a RulesetAssurance with identity, hash, kind and readiness |
CompetenceYearResult exposes the same four fields for the whole year: each axis is the
worst of its runs, rulesets, blockers and limitations are listed once each, and the year is
payable only when every run is.
Fail-closed payability¶
What a run must cover comes from the capability registry of the fiscal year
(knowledge/capability/registry/<year>.json) and from the run itself:
- every capability that applies to the run (its predicate holds: a core stage, an event the request declares, the run that closes the employment) must be computed in full; otherwise it is a coverage gap;
- every capability whose registry entry declares
applicability_factsis required in every run. It must be ruled out by a decision of the run, or decided on the supplied facts. When no decision rules it out and one of those facts is left to its default, each such fact is anUnresolvedRequirementinresult.capability_report.unresolvedand arequirement_unresolvedblocker. A decision the capability took on other facts does not resolve it: posting an installment of last year's surtax does not decide this year's.
The required capabilities and their applicability facts today:
| Capability | Applicability fact | Ruled out by |
|---|---|---|
addizionale_regionale |
facts.regione |
a supplied region, or an employer that is not a withholding agent |
addizionale_comunale |
facts.comune_belfiore |
a supplied municipality, or an employer that is not a withholding agent |
family_deductions |
facts.family_composition |
a supplied composition (FamilyComposition() states that there is no dependant), or an employer that is not a withholding agent |
A household employer is not a withholding agent (art. 23 c. 1 DPR
600/1973): its run decides that no surtax and no deduction is due, so it
needs neither fact. A resident owes both surtaxes when net IRPEF is due
(D.Lgs. 446/1997 art. 50 c. 2, D.Lgs. 360/1998 art. 1 c. 4), so an unknown
residence cannot stand for "no surtax". The run also records a
residence_unknown decision on each surtax whose code is missing
(incomplete, amount None, inputs["fact"] naming the fact), so its
calculation is incomplete too; see
Surtax decisions.
The registry rejects an applicability fact that is not one of the facts the
capability reads, one on a capability that is not decided, and one the
request has no reader for.
Payability rules¶
A result is payable only when it has no blocker. Each of the following adds one:
BlockerCode |
feature |
detail |
Raised when |
|---|---|---|---|
calculation_issue |
None |
issue code | The run raised an issue (an assumption, a fallback, an unrecovered shortfall) |
calculation_issue |
capability | decision reason | A decision is not final |
missing_fact |
None |
fact name | An issue names a fact the calculation needs and the request did not supply |
requirement_unresolved |
capability | fact path | No decision ruled out a required capability and its applicability fact (e.g. facts.regione) was left to its default |
capability_not_computed |
capability | gap kind | A capability that applies to the run is unsupported, unresolved or partial |
rule_source_weak |
capability | assumed or missing |
An executed capability read a rule weaker than the evidence its registry entry accepts (derived for every capability today), or no rule of the run carries a record |
caller_supplied_rule |
capability | field names | The caller supplied a rate or multiplier in place of a bundled rule |
open_limitation |
capability | limitation id | An open model limitation with monetary impact yes or unknown applies to the run |
run_not_computed |
rule name (base_salary) |
run id | Competence or tax year only: a run was left out because the bundle holds no base salary of its level on its competence date; see ContractSummary.validity |
ruleset_conflict |
None |
id@version |
Competence or tax year only: two runs read the same ruleset version with a different hash, kind, readiness or confidence (a custom repository whose data changed between runs); both contents stay in rulesets |
ruleset_not_production |
None |
ruleset id | operational mode only: a ruleset that tracks readiness (today, the CCNL) is not production with a verified confidence; no_ruleset_tracks_readiness when the CCNL has no ruleset identity |
A derived rule (taken from a cited document location, with no recorded
human check) lowers the evidence axis but does not block on its own. Only
1696 payable rules of
the bundle are verified, so requiring verified would block almost every
result; the evidence axis keeps the difference visible instead.
Each blocker also carries a remediation sentence for a human reader. Branch
on code, feature and detail, not on the sentence.
What the bundle gives¶
A result is payable when every fact it reads is stated and every rule it
executes is at least derived. Two CCNLs reach it in CI: Metalmeccanico
Federmeccanica, the production ruleset, in operational mode, and Concia
UNIC, a reviewed one, in simulation
(tests/integration/ccnl_engine/api/test_engine_modes.py).
Stating only the common facts is not enough. For the first level of each CCNL, a regular run of June 2026 with no event, for a new hire whose residence and empty family are stated, is not payable:
- a fact the engine does not assume is still missing: the opening state of the year, the other employers and the income of the current year, the TFR destination of a company under the Tesoreria rule;
- a rule the run executes is
assumed: the municipal surtax of a comune with no delibera for the year and, for most CCNLs, the base salary, since
92 of 132
CCNLs cite no clause for their number of monthly payments;
- for some CCNLs, an open_limitation that concerns every ordinary month (an
INPS rate reused from another sector, a salary table read from a proxy
source).
Without the residence or the family, each run also carries a
requirement_unresolved blocker per unknown fact. None has a coverage gap:
the capabilities the engine does not compute (INAIL, health funds, maternity,
...) do not apply to an ordinary month or are outside the request (see the
capability matrix). The bundle holds
59 missing rules
(see Provenance); a run that reads one also
raises a rule_source_missing issue and is incomplete.
Use an amount for payment only when its result says is_payable. In
operational mode every run also carries a ruleset_not_production blocker
unless its CCNL is production (see
Readiness).
Defaults of the public inputs¶
Every field with a default in the public input types (the request and plan
types of ccnl_engine, the facts of ccnl_engine.inputs, the events of
ccnl_engine.events) is classified in
ccnl_engine.default.policies, and
tests/integration/ccnl_engine/default/test_policies.py
fails on a defaulted field
without a classification, or on a classification without its field:
absence_is_fact: the default is the fact. No event happened, the employment has not ended, the worker waived no regime, declared no certified disability and no sole-parent condition, and exercised no contributory option.requires_fact: the default stands for a fact the caller has not stated. Each entry names the capability the fact feeds and how a run honours it:requirement: an applicability fact of the registry (residence, family composition), so arequirement_unresolvedblocker;reported: the run reports the fact as missing when the capability needs it, with amissing_factblocker or an input error (seniority, category, contribution history, sector, employer activity, prior-year income, current-year income with a dependant, signing date of a renewal, reference period of renewal arrears, contributable hours of a domestic CCNL, a child's birth date, the own income, residency, cohabitation and share of a dependant that may qualify for an art. 12 TUIR deduction, the apprenticeship track among several, the additional 1% IVS other employers withheld when their base is imported and a run settles the 1%, the opening state of a run that is not the first of an employment whose start is stated, the INPS base of other employments when the INPS rules carry a massimale or a 1% threshold, the TFR fund at 31 December of the year before on a December run, the Fondo Tesoreria destination of the TFR, the first day of the imported sickness history and the exemption of a short absence when a CCNL counting several sickness episodes needs them, the income beyond the employment when a somma esente is due, the full time of an employment whose weekly hours are stated, the roles of a worker on a level with an allowance restricted to a role, the enrolment in a pension fund of every CCNL but domestic work, the suspension of accrual of an absence that could change a rateo, the renewals and the NASpI exclusion of a fixed-term contract);pending: not honoured yet. The default still selects a branch without a blocker. Treat these fields as required and state them.
The pending fields:
| Field | What the default does today |
|---|---|
Employment.employment_period |
A full month and full ratei, even for a hire or a termination within the month (a run after the first still needs its opening state) |
Employment.weekly_hours |
Full time (a domestic CCNL raises without it) |
A pending field becomes requirement or reported when its default can
be told apart from a stated value (a field that defaults to True or 0
cannot) and the run blocks on it, or leaves the registry when its default
is removed. For every requires_fact field, the run that states the fact
has at most the blockers of the run that leaves it to its default
(tests/integration/ccnl_engine/api/test_default_facts.py, on the request pairs
of tests/integration/ccnl_engine/default/builders.py): a false default is never the one
path that looks payable. Dependent.dependent_from and dependent_until have no
default: None states an open end. OpeningBalances.inps_bases, surtax_obligations and
recoveries have no default: an import states what the previous provider
determined, () when there is nothing, and a closed run of a competence
year without its INPS base is an input error. The opening state is judged
on its content, not on the default: PeriodState.zero() passed
explicitly after the start of the employment blocks as the default does,
and so does every run that descends from it (PeriodState.history_known
is False on its closing state). See
Opening state and imported balances.
Model limitations¶
A known simplification of the model is data, not a comment. The registry has
382 ModelLimitation
entries: one per simplification note of a CCNL file that can move an
amount, and 9
engine limitations of code paths several CCNLs share
(knowledge/limitation/engine.json: the apprenticeship midpoint and
the apprentice seniority increment, both resolved; a percentage
apprenticeship reducing an allowance whose apprenticeship_pct_relevant flag
the data leaves at its default; two sickness paths).
A CCNL whose midpoint components are unsourced carries its own
<ccnl_id>/apprenticeship_midpoint_components limitation. A CCNL that declares no
apprentice seniority amount carries its own <ccnl_id>/apprentice_seniority
limitation, recorded when an apprentice has matured increments the level
pays. Each entry has a stable id, the capability and variant it
limits, the rulesets and dates it affects, a monetary_impact (yes,
no, unknown), a status (open, resolved), its source and a
remediation.
The 439 simplification notes of the bundle each state their impact on what the engine computes from the bundle:
166 yes,
207 unknown
and 66 no (the
engine refuses the case, or takes the value from the caller). A file whose
note can move an amount without declaring a limitation does not load, so the
bundle build fails on an unmapped note.
A limitation applies to a run only when its predicate holds. Its trigger classifies it by obligation:
- 31
limitations are
owed: an amount the CCNL owes for every worker it scopes that the engine does not compute (a health or bilateral fund, a provincial element owed in every province). They apply to every run of the CCNL within their scope, whatever capability the run applies; - otherwise the capability it limits applies to the run (an overtime limitation needs an overtime event);
- the run date, contract type, level, worker category, run kind and seniority
fall within its
applies_whenscope (an unknown category or seniority does not rule a run out); - an engine limitation applies only when the run takes its code path, and the run records the traversal (an apprentice in a midpoint period of a CCNL whose midpoint components are unsourced);
- 85
limitations depend on a fact the request cannot express (a hire date before
a transitional regime, a fund the worker joins, a sector section): they are
documented on the contract page and never recorded on a run. An amount
owed for every worker is never
outside_input, even when the request has no field for it: it isowed.
Every applicable limitation is listed in result.assurance.limitations; an
open one with impact yes or unknown adds an open_limitation blocker.
The limitation names the totals it can make wrong (net_pay,
employer_cost, both by default) and its blocker carries them: a fund only
the employer pays leaves is_employer_cost_complete false and
is_net_pay_payable true, and is_payable false.
Open limitations with an impact also lower the capability they limit to
partial in the capability matrix, and
each contract page lists its limitations under "Known simplifications".
Assurance axes¶
| Axis | Type | Derived from |
|---|---|---|
calculation |
CalculationStatus |
Worst status of result.issues and result.decisions; final when there are none |
coverage |
CoverageStatus |
result.capability_report.status: gaps and unresolved requirements |
evidence |
EvidenceStatus |
Weakest provenance of the payable rules the run read (verified, derived, assumed, missing); missing when none carries a record |
rulesets |
tuple[RulesetAssurance, ...] |
Identity, version, hash, kind, readiness and confidence of the CCNL ruleset and of each ruleset a payable rule was read from |
mode |
EngineMode |
simulation (default) or operational, from the engine |
payability |
Payability |
payable exactly when blockers is empty; is_net_pay_payable and is_employer_cost_complete read the totals of each blocker |
limitations |
tuple[ModelLimitation, ...] |
The model limitations that apply to the run, open or resolved, whatever their impact |
Calculation¶
| Status | Meaning |
|---|---|
final |
Every capability decided from known rules and facts. |
provisional |
Computed, but a decision rests on an assumption that may change the amounts. |
incomplete |
At least one amount could not be determined. |
rejected |
The inputs cannot produce a meaningful result. |
result.decisions records what each capability decided and on which rule, so
a result can be explained line by line whatever its status.
Issues that lower the calculation axis include:
| Code | Status | When |
|---|---|---|
rule_source_missing |
incomplete |
An executed capability read a payable rule whose provenance status is missing |
employer_rate_category_assumed |
provisional |
The sector sets INPS employer rates by worker category (artigianato: impiegati and quadri 24.71%), the level fixes no category and none was declared, so the general rate (26.93%, the operai rate) applied |
somma_esente_band_assumed |
provisional |
The worker has employment income from other employers this tax year; the percentage of the somma esente is taken on the income of this employer alone |
withholding_shortfall_unrecovered |
provisional |
The pay of the run cannot cover the tax due; the worker must be told the amount |
Coverage¶
result.capability_report compares the capability registry of the fiscal
year with what the calculation observed. The registry is the single source
of coverage: the same entries drive the contracts index and the
capability matrix. Each entry declares
its implementation (native, caller_supplied, partial, unsupported),
an applicability predicate, the handler that decides and traces it, the
facts it reads and the weakest evidence its rules may have.
capability_report.scope gives each capability as applicable,
not_applicable or outside_input for the run. Only an applicable
capability can leave a CapabilityGap, and each gap is a
capability_not_computed blocker. capability_report.unresolved lists the
required capabilities left undecided by a default (see
Fail-closed payability).
status |
When |
|---|---|
complete |
No gaps and no unresolved requirement |
partial |
Every gap is a partial_result (a capability implemented in full came out partial) or a partial_implementation (sickness, foreign tax credit, ... executed) |
incomplete |
An unresolved requirement, or any other gap: an applicable capability is unsupported (residual leave on the run that closes the employment) or unresolved |
rule_sources maps each executed capability that reads bundled rules to the
weakest provenance status among them. Capabilities computed only from
caller-declared amounts do not appear.
caller_supplied maps each capability that used a rate or an amount the
caller supplied in place of a rule (an overtime hourly rate or explicit
multiplier, a sickness integration rate) to the event fields it took; see
Decisions.
Reading the assurance¶
from datetime import date
from ccnl_engine import (
EmployerProfile,
Employment,
Headcount,
PayrollEngine,
PayrollRun,
PeriodFacts,
PeriodInput,
)
from ccnl_engine.inputs import Permanent
engine = PayrollEngine.bundled()
result = engine.calculate_period(
PeriodInput(
run=PayrollRun.regular(year=2026, month=1),
payment_date=date(2026, 1, 28),
employment=Employment(
ccnl_slug="metalmeccanico-federmeccanica.json",
level_code="C3",
contract_type=Permanent(),
),
employer=EmployerProfile(headcount=Headcount(50)),
facts=PeriodFacts(regione="IT-25"), # municipality and family unknown
)
)
if not result.is_payable:
for blocker in result.blockers:
print(blocker.code.value, blocker.feature, blocker.detail)
# among others (the seniority is unknown too):
# → requirement_unresolved addizionale_comunale facts.comune_belfiore
# → requirement_unresolved family_deductions facts.family_composition
for requirement in result.capability_report.unresolved:
print(requirement.feature, requirement.fact)
assurance = result.assurance
print(assurance.calculation, assurance.coverage, assurance.evidence)
for ruleset in result.rulesets:
print(ruleset.id, ruleset.kind, ruleset.readiness, ruleset.source_hash[:12])