Ga naar hoofdinhoud

The compliance rule engine

Alongside the register data layer, HRMQ ships a versioned, static corpus of operative HR/labour and payroll rules — derived from EU labour directives, ILO core conventions, GDPR for employee data, occupational health & safety, national labour law (NL first), and payroll / wage-tax & social-security compliance. An executable RuleEngine audits the register's HR/labour objects against that corpus and reports compliance coverage.

Rules are facts, not config

Rules are facts about directives and laws — identical for every tenant, changing only with regulation — so they ship as versioned static data, one JSON file per sub-domain under lib/Standards/rules/, not as OpenRegister per-tenant configuration:

  • payroll.json — tax and reporting rules (loonaangifte, UPA pension filing, GL posting, engine traceability)
  • labour.json — labour law (leave, verzuim/Poortwachter, Arbeidstijdenwet, onboarding/offboarding, org-chart and asset integrity, performance dossiervorming)
  • privacy.json — GDPR/AVG rules (recruiting-data retention)

RuleCatalogue (a read-only, versioned accessor, RuleCatalogue::VERSION) loads and merges these files and exposes query helpers. Bump the version on any change to a rule file.

The rule shape

Every rule declares:

FieldMeaning
idstable slug, unique across the whole corpus
domainsub-domain key (labour, payroll, reporting, gdpr-recruitment, ...)
jurisdictionISO 3166-1 alpha-2, EU, or global
frameworkthe law/framework slug (e.g. bw7-10, nl-poortwachter, nl-arbeidstijdenwet, gdpr)
sourcehuman citation
statementthe operative rule statement
severitymandatory, conditional, or recommended
machineCheckabletrue only when a deterministic program can decide compliance from structured fields

machineCheckable is set true only when compliance is decidable from structured fields — presence, format, arithmetic, enumeration, cardinality, date-window, or referential checks. Narrative or judgemental rules (disclosures, "reasonable", policy text) are false. The audit coverage metric is enforced ÷ machineCheckable, so over-flagging silently deflates coverage — the corpus is expected to flag honestly.

Enforcement: CheckProviders

A rule becomes enforced when a CheckProvider under lib/Standards/Checks/ registers a predicate keyed by that rule's id. Every CheckProvider implementation is auto-discovered by RuleEngine::providers() — adding a new compliance domain means adding a new provider file, never editing RuleEngine itself. Each predicate is a pure, side-effect-free function fn(array $object, array $context): bool that returns true when the rule is satisfied for that object.

The engine only enforces rules that have both a corpus entry and a registered predicate — the rest of the corpus is catalogued but not yet executable, and grows an executable check per development wave. Every provider referenced throughout this documentation — NlPayrollChecks, NlEngineChecks, NlWageTaxFilingChecks, NlPensionFilingChecks, NlGlPostChecks, NlNetPayChecks, NlLeaveChecks, NlVerzuimChecks, NlAttendanceChecks, NlOnboardingChecks, NlOffboardingChecks, NlAtsChecks, NlPerformanceChecks, NlOrgChecks, NlAssetChecks, NlDocumentChecks, NlSignalChecks, NlFleetChecks, NlDgaChecks, NlWkrChecks, NlLoonbeslagChecks, NlReiskostenChecks, NlRosterChecks, CompChecks — is one of these providers.

The corpus has grown well past its original payroll/labour/privacy scope: it now also covers company-car bijtelling, DGA gebruikelijkloon, werkkostenregeling eindheffing exposure, wage garnishment (loonbeslag), mileage-rate reimbursement, retroactive adjustment consistency, roster ATW pre-checks, and within-band compensation proposals — each backed by one of the providers above, each documented on its own capability page.

Applicability is jurisdiction-scoped: a rule applies to its own country, plus EU-wide rules for EU members and global rules everywhere, so the engine only enforces what an organisation is actually subject to.

Running the audit

occ hrmq:rules:audit --jurisdiction=NL

The report includes:

Hrmq rule-compliance audit
catalogue version : 2026-07.13
corpus rules : <N> (machine-checkable: <N>)
enforceable today : <N> (<NN.N>% of machine-checkable)

objects checked : <N>
compliant : <N>
with violations : <N>
violations : <N> mandatory / <N> conditional / <N> recommended
<ObjectType> checked=<N> compliant=<N> withViolations=<N>
...

top violated rules:
<rule-id> <N>

Exit code is non-zero for occ hrmq:payroll:verify (the run-scoped payroll audit) on any mandatory violation; the general hrmq:rules:audit command is a read-only reporting tool.

Seeding test data

occ hrmq:rules:seed-testdata

Idempotently backfills local test data so the enforced rules have something to check — provider sample objects and provider field defaults, applied by RuleTestDataSeeder from each provider's seedSpec().

Where this shows up elsewhere

Nearly every capability documented elsewhere in this site is backed by one or more corpus rules — see The payroll engine, Retroactive adjustments, Sick pay, Company car bijtelling, DGA payroll mode, Loonbeslag, Werkkostenregeling, Loonaangifte filing, Pension filing, Leave & verzuim, Time & attendance, Rostering, Expense claims, Onboarding & offboarding, Recruiting, Compensation cycles, Performance reviews, Org chart, Documents, Assets, and AVG data-subject rights — for the specific rules each one adds.