The example corpus#
Two corpora, one engine (ADR-0003):
the schema coverage corpus proves what each bundled XSD can express: every element path and every choice branch of all thirteen editions appears in at least one file of that edition’s set, every file is schema-valid and passes the ISO MDR cross-element rules;
the market corpus is what a bank accepts: one scenario per country and rail, rendered into every edition it lists, validated on a four-rung ladder and shipped with a provenance record.
Both ship in the wheel under pain001/corpus/data/ (230 KB compressed of a
400 KB budget) and are read through pain001.corpus:
from pain001.corpus import list_files, get_file, provenance, coverage_report
for entry in list_files("market"):
print(entry.scenario_id, entry.version, entry.country, entry.family)
xml = get_file("gb.chaps.property-purchase", "pain.001.001.09")
record = provenance("gb.chaps.property-purchase", "pain.001.001.09")
print(record["provenance"]["confidence"], record["validation"]["profiles"])
print(coverage_report("pain.001.001.13")["paths"]["percent"]) # 100.0
How to read a scenario#
A scenario is a YAML file under scenarios/<country>/ that says what a
payment is; the builder decides how each edition spells it. The schema is
pain001/corpus/schema/corpus.schema.json; the loader reports the JSON
pointer of the first problem.
id: gb.chaps.property-purchase # country.rail.name
family: priority-payment # the rail family overlays target
country: GB
versions: [pain.001.001.03, pain.001.001.09]
seed: 20260912 # every auto identifier derives from it
profiles: [uk-chaps, purpose-mandate-gb, anti-duplicate] # rung L2
parties: # named parties a ref can point at
uk-corporate:
name: Elm Road Developments Ltd
address: {form: hybrid, street: Elm Road, building_number: "14",
post_code: SW1A 1AA, town: London, country: GB}
id: {org: {lei: auto}} # auto: a checked synthetic LEI
header: {message_id: ELMRD-20260912-0001, created: "2026-09-12T08:30:00",
initiating_party: {ref: uk-corporate}}
payment:
type: {priority: HIGH, service_level: SDVA}
requested_execution_date: "2026-09-12"
debtor: {ref: uk-corporate}
debtor_account: {iban: auto} # a mod-97-valid GB IBAN
debtor_agent: {bic: auto, clearing: {system: GBDSC, member_id: "040004"}}
charge_bearer: SHAR
transactions:
- end_to_end_id: COMPLETION-2026-0912
uetr: auto
amount: {ccy: GBP, value: "425000.00"}
creditor: {ref: uk-solicitor}
creditor_account: {iban: auto}
purpose: HLST
remittance: {unstructured: "Completion 14 Elm Road"}
provenance:
sources: [{title: "Bank of England: ISO 20022 purpose codes for CHAPS", read: "2026-09-10"}]
confidence: derived # verified | derived | assumed
evidence: [] # filled by the evidence workflow
Friendly keys cover the blocks every market file needs: header, rail choices,
parties with structured, hybrid or unstructured addresses and organisation or
private identification, accounts, agents, transactions, remittance
(unstructured and structured with creditor references), mandates for direct
debits. A raw object at any level carries ISO-named elements verbatim for
the long tail, and is fitted to the edition like everything else.
The builder fits one tree to each edition from the schema inventory:
BIC/BICFI and BICOrBEI/AnyBIC are renamed, scalars are wrapped into
the choices later editions introduced (AdrTp, ReqdExctnDt, Frqcy),
repeats are cut to the edition’s cap, elements the edition lacks are dropped,
and every move is written into the provenance sidecar’s build block.
Identifiers marked auto are synthetic but structurally valid: IBANs in the
country’s BBAN shape with its national check digits, test-form BICs (0 as
the eighth character), ISO 17442 LEIs under an unassigned prefix, v4-shaped
UETRs. Real public routing codes (sort codes, BLZ, ABA numbers) are fine to
use and the scenarios do; account parts are synthetic.
The validation ladder#
Every built file passes four rungs before it ships, and the result is written
into its .provenance.yaml:
Rung |
What |
Where |
|---|---|---|
L0 |
the edition’s XSD |
|
L1 |
the ISO MDR cross-element rules (20 for pain.001, 9 for pain.008) |
|
L2 |
the scheme and rail profiles the scenario lists |
|
L3 |
the overlays that apply to the scenario or its family |
|
make corpus-coverage re-runs L1 to L3 over the shipped files and the
coverage gate in CI; make corpus-build regenerates everything and
scripts/build_corpus.py --check proves a rebuild is byte-identical.
Bank variants and overlays#
An overlay is a short, cited rule set a scheme body, a regulator or a bank
layers on top of the XSD, in the grammar shared with
iso20022-bank-profile-mcp (required, forbidden, equals, one_of,
max_length, matches, charset, and if:<elem>[=<value>]:<verb>). Two
kinds exist:
a rule-only overlay (the Bank of England CHAPS rules, the Febelfin, Currence, CFONB and AEB conventions under
scenarios/overlays/) judges every file of the scenarios it targets;an overlay with a
patchpins a bank’s fixed choices, so the build renders a second file<scenario>__<overlay>.<version>.xmlbeside the generic one and judges that variant by the overlay’s rules.patches:keyed by scenario id adds what a single scenario needs,"*"against a list applies to every transaction, andnullremoves a key.versions:lists the editions the overlay covers: a guideline written for pain.001.001.03 spellsBICand must not judge a .09 file.
The repository ships public overlays only. Bank usage guidelines are the bank’s documentation under its own terms, so no overlay derived from one, no variant built from one and no citation of one is committed here or published on the website. The variant mechanism exists so that you can apply your own bank’s guideline privately:
from pain001.corpus import list_files, get_file
from pain001.corpus.builder import build
from pain001.corpus.registry import load_scenarios
from pain001.corpus.rules.overlays import load_overlays
# your overlays live outside this checkout
mine = {o.overlay_id: o for o in load_overlays("/secure/my-bank-overlays")}
scenario = {s.id: s for s in load_scenarios()}["gb.fps.single"]
variant = build(scenario, "pain.001.001.03", mine["gb.mybank.fps"].patch_for(scenario.id))
scripts/build_corpus.py --market-root /secure/out renders such variants
into a tree of your own when the overlays are placed under a private
scenarios/overlays/ copy passed with --scenarios.
Deriving an overlay from a usage guideline#
Banks publish their guidelines on MyStandards or their client portals
under restricted terms. Those exports (PDF, XSD, Excel, zip) never enter
this repository: .gitignore blocks their names and
tests/test_no_restricted_material.py fails the build on any tracked file
that looks like one, or that carries a MyStandards signature. The private
workflow is:
download the guideline from your bank or financial organisation under your own agreement and keep it in your own storage, outside any public checkout;
run
poetry run python scripts/derive_overlay.py GUIDELINE.xsd --base pain.001.001.03, which inventories the guideline schema, diffs it against the bundled ISO edition and prints removed elements, elements made mandatory, capped repeats, narrowed code lists, shortened lengths and changed patterns (--as-rulesprints them in overlay syntax); it refuses a path inside the repository and writes nothing;keep the resulting overlay and the variants it builds in your own environment. Do not contribute them here.
How to add a country or rail#
Write the scenario under
scenarios/<cc>/<rail>.<name>.yamlfrom the public scheme rules; cite every document underprovenance.sourceswith the date read.List the rail profile under
profiles:. If the rail has none yet, add aRailtopain001/validation/rails.pywith its sources; what the scheme document states is an error, what is typical bank practice is a warning.Where a public bank guide adds rules, write an overlay under
scenarios/overlays/withapplies_to: [<family or scenario id>]and asourceblock. The grammar is the oneiso20022-bank-profile-mcpuses, extended withforbidden,one_of,max_length,matchesandcharset.make corpus-build, thenmake corpus-coverage. A file that cannot pass the ladder is not shipped; it becomes an open question in the scenario’s provenance rather than a file with a caveat.
Evidence policy#
XSD validity is necessary, not sufficient. Each market file carries an evidence state (ADR-0003, decision 7):
bank-validated: a bank’s client validation service (for example through MyStandards), run privately by whoever holds that access; only the result is recorded, never the guideline;portal-validated: the public SIX portal (Swiss files) and ValidateFin (SEPA files);self-validated: this repository’s ladder only, which every shipped file passes; the default for every other market.
make corpus-evidence prints, per scenario, the validator the plan expects
and what has been recorded. An external result is recorded into the scenario
file, the source of truth, and copied into the sidecars by the next build:
poetry run python scripts/corpus_evidence.py record de.sepa.sct-salary \
--state portal-validated --tool ValidateFin --date 2026-09-12 --result pass
Rulebook watch#
The rulebooks moved three times in the three weeks before ADR-0003. Dates that change what a scenario must carry, and the scenarios each affects:
Due |
Change |
Scenarios affected |
|---|---|---|
Oct 2026 (announced) |
EPC sets a new unstructured-address end date |
every |
15 Nov 2026 |
DK DFÜ-Abkommen Anlage 3 V26.11 (CCU gets its own order type) |
|
14 Nov 2026 |
SIX Swiss Payment Standards 2026 (v2.3) |
|
Nov 2026 |
Singapore structured town and country |
|
Autumn 2026 |
Bankgirot platform migration |
|
Dec 2026 (announced) |
Swift CBPR+ timetable revision |
|
30 Sep 2028 |
LSV+/BDD end (Switzerland) |
Swiss direct-debit scenarios (0.0.69) |
What ships when#
0.0.67: the engine (inventory, builder, identifiers, overlay grammar, MDR rules, rail profiles, ladder), the coverage corpus for all thirteen editions, three market scenarios (GB CHAPS, DE SEPA SCT, NL SEPA SDD).
0.0.68: the five tier-1 market packs and the website corpus page. UK (Faster Payments, Bacs Direct Credit and Direct Debit, CHAPS, an international USD payment), the SEPA core countries (DE, FR, NL, BE, ES, IT, LU with each country’s reference convention and the German DK order types), US (ACH, EFTPS, Fedwire, RTP, cheque), CH (the Swiss Payment Standards types D, S and X, QR-bill and SCOR references) and SE (Bankgiro with OCR, Plusgiro, salary, RIX), every file from public rulebooks. Bank variants are built privately from your bank’s guideline.
0.0.69: tiers 2 and 3 (CZ, LU, HK, SG, MY, QA, AE) with confidence chips, the CSV pipeline extension for the twelve most-used rails, and the pain.001 twin foundation: the ISO JSON twin beside every pain.001 market file, one JSON Schema per edition, and the records twin whose gap list drives the pipeline work (twins, ADR-0005).