Each rule module is now a triple plus a version pin:
rules/<module>.catala_en # spec — Catala source (authoritative)
rules/<module>_eval.py # impl — pure-Python reference evaluator
rules/<module>_conformance.py # cross-check — asserts spec ≡ impl
rules/<module>_source.yaml # version pin — official source URL + sha256
rules/<module>_metadata.json # human label + certification state (existing)
rules/<module>__<scope>.schema.json # I/O JSON Schema (existing, generated)
Catala remains the spec. The Python evaluator is the runtime — it lets a user run the rule without installing opam, which matters for both casual users and CI in environments where opam is heavy.
- Catala is the legal-readable spec. It is the document that a lawyer
reviewing the rule will read. It compiles to a typechecked DSL; the
#[test]scopes in the file run on every push. - Python is the operational runtime. It is what most consumers of the module will actually invoke. It must agree with Catala, byte for byte, on canonical inputs.
- Conformance is the gate. CI fails if Python and Catala disagree.
All 12 modules now have catala + eval.py + conformance.py + source.yaml.
All 12 conformance tests pass under python3 (Catala cross-check is
skipped locally where opam is not installed; CI exercises the Catala
side via catala interpret --no-stdlib).
| Module | catala | eval.py | conformance | source.yaml | drift |
|---|---|---|---|---|---|
| difc_rdc_part_38 | ✓ | ✓ | ✓ | ✓ | http_get |
| difc_practice_direction_4_2017 | ✓ | ✓ | ✓ | ✓ | http_get |
| difc_rdc_38_19_indemnity | ✓ | ✓ | ✓ | ✓ | http_get |
| difc_third_party_disclosure | ✓ | ✓ | ✓ | ✓ | http_get |
| uae_civil_code_art_390 | ✓ | ✓ | ✓ | ✓ | http_get (MoJ index page) |
| adgm_cpr_admissions | ✓ | ✓ | ✓ | ✓ | http_get (PDF, binary sha256) |
| adgm_cpr_summary_judgment | ✓ | ✓ | ✓ | ✓ | http_get (PDF, binary sha256) |
| adgm_arbitration_regulations_2015 | ✓ | ✓ | ✓ | ✓ | http_get |
| english_contract_interpretation | ✓ | ✓ | ✓ | ✓ | manual (doctrine) |
| ladd_v_marshall | ✓ | ✓ | ✓ | ✓ | manual (doctrine) |
| sg_iaa_s_31 | ✓ | ✓ | ✓ | ✓ | http_get |
| caparo_three_stage_test | ✓ | ✓ | ✓ | ✓ | manual (doctrine) |
Drift coverage: 9 of 12 modules under active http_get drift detection
(6 HTML + 2 PDF + 1 MoJ-roster index). The 3 manual modules are
common-law doctrines where drift = higher-court overrule, not text
amendment — manual is the correct method, not a gap.
- Read the corresponding
.catala_ento find the scope name(s) and the input/output structures. - Copy the relevant function out of
spike/trace-NN/evaluate.py. - Convert it to a pure function:
(input dict) → output dict— drop any event-log handling. Usedecimal.Decimal(str(x))for numerics. - Write
<module>_conformance.pywith at least:- One synthetic case from the
.catala_en#[test]scope. - One real case from the trace that uses the module.
- One synthetic case from the
- Run
python3 rules/<module>_conformance.pylocally — must pass. - Push; CI runs all
*_conformance.pyand--softdrift check.
scripts/check_rule_drift.py fetches the pinned URL for each module,
canonicalises the HTML (strip markup, collapse whitespace, lowercase),
sha256s it, and compares against the retrieved_sha256 in
<module>_source.yaml.
--bootstrappopulatesretrieved_sha256where currently null.--check(default) exits non-zero on drift or unpinned modules.--softexits zero (CI-friendly while URL coverage matures).
For doctrines (Caparo, Ladd v Marshall, Wood v Capita), drift detection
is manual — drift means a higher court overrules the doctrine, not a
text amendment. The drift checker prints a "MANUAL" line with the URL
to inspect on each run as a reminder.
For UAE Civil Transactions Law and ADGM CPR rule books, the official
URLs are either subscription-walled (Thomson Reuters mirror) or
intermittently slow; those are also tagged manual until a stable
free authoritative URL is identified. See drift_block_reason in
each affected _source.yaml.
Each <module>_source.yaml records:
version_id: the in-force version of the source instrument the rule is encoded againstsource_authority.url: the canonical URLsource_authority.retrieved_sha256: hash of the canonicalised text at retrieval; populated by--bootstrapamendment_window: in-force-from / in-force-until datesexpiry.{reminder_at, hard_expiry_at}: dates after which the module must not be relied on without re-pinning
This is the audit trail. If anyone ever asks "which version of RDC 38 is this rule encoded against?", the source.yaml is the answer.