cbpr-usage-rules 0.4.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/CHANGELOG.md +21 -0
  2. {cbpr_usage_rules-0.4.0/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.6.0}/PKG-INFO +44 -2
  3. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/README.md +43 -1
  4. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/__init__.py +19 -1
  5. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/cli.py +49 -1
  6. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/engine.py +51 -2
  7. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.max.xml +79 -79
  8. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.052.min.xml +1 -1
  9. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.max.xml +78 -78
  10. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/camt.054.min.xml +1 -1
  11. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.002.max.xml +17 -17
  12. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.002.min.xml +3 -3
  13. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.max.xml +49 -49
  14. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.004.min.xml +3 -3
  15. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.max.xml +41 -41
  16. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008.min.xml +3 -3
  17. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml +41 -41
  18. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml +6 -6
  19. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.009.max.xml +37 -37
  20. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.009.min.xml +3 -3
  21. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml +41 -41
  22. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pacs.009_adv.min.xml +3 -3
  23. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml +53 -53
  24. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml +3 -3
  25. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2025/pain.001.max.xml +28 -28
  26. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2026 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2025}/pain.001.min.xml +2 -2
  27. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.max.xml +79 -79
  28. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.052.min.xml +1 -1
  29. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.max.xml +78 -78
  30. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/camt.054.min.xml +1 -1
  31. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.002.max.xml +17 -17
  32. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.002.min.xml +3 -3
  33. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.max.xml +45 -45
  34. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.004.min.xml +3 -3
  35. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.max.xml +41 -41
  36. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008.min.xml +3 -3
  37. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml +41 -41
  38. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml +6 -6
  39. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.009.max.xml +37 -37
  40. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.009.min.xml +3 -3
  41. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml +41 -41
  42. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pacs.009_adv.min.xml +3 -3
  43. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml +53 -53
  44. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml +3 -3
  45. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/examples/y2026/pain.001.max.xml +27 -27
  46. {cbpr_usage_rules-0.4.0/src/cbpr_rules/examples/y2025 → cbpr_usage_rules-0.6.0/src/cbpr_rules/examples/y2026}/pain.001.min.xml +2 -2
  47. cbpr_usage_rules-0.6.0/src/cbpr_rules/idgen.py +243 -0
  48. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/message.py +81 -10
  49. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/registry.py +3 -9
  50. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +44 -2
  51. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/SOURCES.txt +3 -1
  52. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/tests/test_examples.py +24 -8
  53. cbpr_usage_rules-0.6.0/tests/test_idgen.py +79 -0
  54. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/LICENSE +0 -0
  55. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/MANIFEST.in +0 -0
  56. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/pyproject.toml +0 -0
  57. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/setup.cfg +0 -0
  58. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/helpers.py +0 -0
  59. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/loader.py +0 -0
  60. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/models.py +0 -0
  61. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/py.typed +0 -0
  62. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/__init__.py +0 -0
  63. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/countries.py +0 -0
  64. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/reference/currencies.py +0 -0
  65. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/__init__.py +0 -0
  66. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
  67. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/camt_052.py +0 -0
  68. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/camt_054.py +0 -0
  69. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_002.py +0 -0
  70. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_004.py +0 -0
  71. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_008.py +0 -0
  72. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +0 -0
  73. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009.py +0 -0
  74. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +0 -0
  75. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +0 -0
  76. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2025/pain_001.py +0 -0
  77. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
  78. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -0
  79. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/camt_054.py +0 -0
  80. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_002.py +0 -0
  81. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_004.py +0 -0
  82. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_008.py +0 -0
  83. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +0 -0
  84. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009.py +0 -0
  85. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +0 -0
  86. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +0 -0
  87. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/rules/y2026/pain_001.py +0 -0
  88. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/schema.py +0 -0
  89. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/__init__.py +0 -0
  90. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/bic.py +0 -0
  91. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/country.py +0 -0
  92. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/currency.py +0 -0
  93. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/iban.py +0 -0
  94. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_rules/validators/lei.py +0 -0
  95. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
  96. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
  97. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
  98. {cbpr_usage_rules-0.4.0 → cbpr_usage_rules-0.6.0}/src/cbpr_usage_rules.egg-info/top_level.txt +0 -0
@@ -4,6 +4,27 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
5
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.0]
8
+
9
+ ### Added
10
+ - Rule listings now show the **affected XPaths** for each rule, so agents can see
11
+ exactly which field(s) a rule constrains (concrete element paths and `@attribute`
12
+ targets, or a `//Name` wildcard). Shown by default in `cbpr-validate --list` /
13
+ `--enforced` (text + JSON) and via `list_rules(..., with_xpaths=True)`. Paths are
14
+ derived by running each rule against the bundled example messages.
15
+
16
+ ## [0.5.0]
17
+
18
+ ### Added
19
+ - Deterministic **ID generation** library (`cbpr_rules.idgen`, "Counting Strings"
20
+ algorithm — no `random`/`uuid`): `generate_iban`, `generate_lei`, `generate_bic`,
21
+ `generate_uuid`/`generate_uetr`, `generate_mid`, plus `counting_string`/
22
+ `counting_sequence` and seeded helpers. Every id is structurally valid and passes
23
+ the package's own validators; same seed → same output. New CLI
24
+ `cbpr-validate --generate {iban,lei,bic,uuid,uetr,mid,text}` (with
25
+ `--seed/--country/--bank/--branch/--count`). The bundled example messages now
26
+ source their identifiers from this library (seeded per example).
27
+
7
28
  ## [0.4.0]
8
29
 
9
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cbpr-usage-rules
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Validate ISO 20022 CBPR+ XML messages against SWIFT usage (business) rules.
5
5
  Author: Pete Houghton
6
6
  License: MIT License
@@ -172,11 +172,34 @@ xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults t
172
172
  cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
173
173
  ```
174
174
 
175
- The data is **fictitious** (synthetic BICs/IBANs/LEIs, `EXAMPLE` names) and does
175
+ The data is **fictitious** (synthetic, generated BICs/IBANs/LEIs) and does
176
176
  not reproduce any real sample — use them as starting templates, not real payments.
177
177
 
178
178
  ---
179
179
 
180
+ ## Generating identifiers
181
+
182
+ The example values above are produced by a small **deterministic** ID generator
183
+ (the "Counting Strings" algorithm — no `random`/`uuid`, same seed → same output).
184
+ Every generated id is structurally valid and passes the tool's own validators.
185
+
186
+ ```bash
187
+ cbpr-validate --generate iban --country AT --seed s1 # ISO 13616 + mod-97
188
+ cbpr-validate --generate lei --seed acme # ISO 17442 + check digits
189
+ cbpr-validate --generate bic --bank JSBP --country GB # -> JSBPGB..XXX
190
+ cbpr-validate --generate uuid --count 3 # RFC 4122 v4 (UETR)
191
+ cbpr-validate --generate mid --country GB # message identifier
192
+ ```
193
+ ```python
194
+ from cbpr_rules import generate_iban, generate_lei, generate_bic, generate_uetr
195
+ generate_iban("DE", seed="acme") # deterministic, valid German IBAN
196
+ generate_bic(country="GB", seed="acme")
197
+ ```
198
+
199
+ IBAN generation supports the 27 EU countries plus GB.
200
+
201
+ ---
202
+
180
203
  ## The result object
181
204
 
182
205
  Both `validate_file` and `validate_string` return a dictionary:
@@ -259,6 +282,7 @@ matching is by local name, so namespace prefixes never matter.
259
282
  cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
260
283
  cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
261
284
  cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True) # only the enforceable rules
285
+ cbpr_rules.list_rules(2025, "pacs.008", with_xpaths=True) # + "xpaths" each rule affects
262
286
  ```
263
287
 
264
288
  ```bash
@@ -267,6 +291,24 @@ cbpr-validate --year 2025 --type pacs.008 --list # all rules
267
291
  cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
268
292
  ```
269
293
 
294
+ The `--list` / `--enforced` output (and `with_xpaths=True`) annotates each rule with
295
+ the **concrete fields it affects**, so you can see exactly which element/attribute a
296
+ rule constrains — handy when several fields share a name (e.g. many `Ctry` fields, but
297
+ a rule targets only one):
298
+
299
+ ```text
300
+ pacs.008:VAL-IBAN - CBPR_Valid_IBAN
301
+ //IBAN
302
+ pacs.008:VAL-BIC - CBPR_Valid_Agent_BIC
303
+ /Document/FIToFICstmrCdtTrf/CdtTrfTxInf/InstgAgt/FinInstnId/BICFI
304
+ pacs.008:R41 - CBPR_Interbank_Settlement_Currency_FormalRule
305
+ /Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt/@Ccy
306
+ ```
307
+
308
+ Paths are derived by running each rule against the bundled example messages
309
+ (best-effort): concrete element paths and `@attribute` targets where present, or a
310
+ `//Name` wildcard for a datatype rule whose element isn't in the examples.
311
+
270
312
  ### Algorithmic field validation
271
313
 
272
314
  Beyond the published rules, the following formats are validated with their
@@ -117,11 +117,34 @@ xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults t
117
117
  cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
118
118
  ```
119
119
 
120
- The data is **fictitious** (synthetic BICs/IBANs/LEIs, `EXAMPLE` names) and does
120
+ The data is **fictitious** (synthetic, generated BICs/IBANs/LEIs) and does
121
121
  not reproduce any real sample — use them as starting templates, not real payments.
122
122
 
123
123
  ---
124
124
 
125
+ ## Generating identifiers
126
+
127
+ The example values above are produced by a small **deterministic** ID generator
128
+ (the "Counting Strings" algorithm — no `random`/`uuid`, same seed → same output).
129
+ Every generated id is structurally valid and passes the tool's own validators.
130
+
131
+ ```bash
132
+ cbpr-validate --generate iban --country AT --seed s1 # ISO 13616 + mod-97
133
+ cbpr-validate --generate lei --seed acme # ISO 17442 + check digits
134
+ cbpr-validate --generate bic --bank JSBP --country GB # -> JSBPGB..XXX
135
+ cbpr-validate --generate uuid --count 3 # RFC 4122 v4 (UETR)
136
+ cbpr-validate --generate mid --country GB # message identifier
137
+ ```
138
+ ```python
139
+ from cbpr_rules import generate_iban, generate_lei, generate_bic, generate_uetr
140
+ generate_iban("DE", seed="acme") # deterministic, valid German IBAN
141
+ generate_bic(country="GB", seed="acme")
142
+ ```
143
+
144
+ IBAN generation supports the 27 EU countries plus GB.
145
+
146
+ ---
147
+
125
148
  ## The result object
126
149
 
127
150
  Both `validate_file` and `validate_string` return a dictionary:
@@ -204,6 +227,7 @@ matching is by local name, so namespace prefixes never matter.
204
227
  cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
205
228
  cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
206
229
  cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True) # only the enforceable rules
230
+ cbpr_rules.list_rules(2025, "pacs.008", with_xpaths=True) # + "xpaths" each rule affects
207
231
  ```
208
232
 
209
233
  ```bash
@@ -212,6 +236,24 @@ cbpr-validate --year 2025 --type pacs.008 --list # all rules
212
236
  cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
213
237
  ```
214
238
 
239
+ The `--list` / `--enforced` output (and `with_xpaths=True`) annotates each rule with
240
+ the **concrete fields it affects**, so you can see exactly which element/attribute a
241
+ rule constrains — handy when several fields share a name (e.g. many `Ctry` fields, but
242
+ a rule targets only one):
243
+
244
+ ```text
245
+ pacs.008:VAL-IBAN - CBPR_Valid_IBAN
246
+ //IBAN
247
+ pacs.008:VAL-BIC - CBPR_Valid_Agent_BIC
248
+ /Document/FIToFICstmrCdtTrf/CdtTrfTxInf/InstgAgt/FinInstnId/BICFI
249
+ pacs.008:R41 - CBPR_Interbank_Settlement_Currency_FormalRule
250
+ /Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt/@Ccy
251
+ ```
252
+
253
+ Paths are derived by running each rule against the bundled example messages
254
+ (best-effort): concrete element paths and `@attribute` targets where present, or a
255
+ `//Name` wildcard for a datatype rule whose element isn't in the examples.
256
+
215
257
  ### Algorithmic field validation
216
258
 
217
259
  Beyond the published rules, the following formats are validated with their
@@ -4,7 +4,7 @@ This package is AI generated. See the README for details and caveats.
4
4
  """
5
5
  from __future__ import annotations
6
6
 
7
- __version__ = "0.4.0"
7
+ __version__ = "0.6.0"
8
8
 
9
9
  from .models import Rule, Severity, Violation
10
10
  from .engine import (
@@ -15,6 +15,16 @@ from .engine import (
15
15
  example_message,
16
16
  example_variants,
17
17
  )
18
+ from .idgen import (
19
+ generate_iban,
20
+ generate_lei,
21
+ generate_bic,
22
+ generate_uuid,
23
+ generate_uetr,
24
+ generate_mid,
25
+ counting_string,
26
+ counting_sequence,
27
+ )
18
28
 
19
29
  __all__ = [
20
30
  "__version__",
@@ -27,4 +37,12 @@ __all__ = [
27
37
  "available",
28
38
  "example_message",
29
39
  "example_variants",
40
+ "generate_iban",
41
+ "generate_lei",
42
+ "generate_bic",
43
+ "generate_uuid",
44
+ "generate_uetr",
45
+ "generate_mid",
46
+ "counting_string",
47
+ "counting_sequence",
30
48
  ]
@@ -9,6 +9,7 @@ import textwrap
9
9
  from typing import List, Optional
10
10
 
11
11
  from . import __version__
12
+ from . import idgen
12
13
  from .engine import (
13
14
  ValidationError,
14
15
  available,
@@ -19,6 +20,29 @@ from .engine import (
19
20
  )
20
21
 
21
22
 
23
+ def _generate(args) -> List[str]:
24
+ """Produce the ``--generate`` output lines (one id per --count)."""
25
+ kind = args.generate
26
+ out: List[str] = []
27
+ for i in range(max(args.count, 1)):
28
+ seed = f"{args.seed}:{i}" if args.count > 1 else args.seed
29
+ if kind == "iban":
30
+ if not args.country:
31
+ raise ValueError("--generate iban requires --country (e.g. --country AT)")
32
+ out.append(idgen.generate_iban(args.country, seed))
33
+ elif kind == "lei":
34
+ out.append(idgen.generate_lei(seed))
35
+ elif kind == "bic":
36
+ out.append(idgen.generate_bic(args.bank, args.country or "GB", args.branch, seed))
37
+ elif kind in ("uuid", "uetr"):
38
+ out.append(idgen.generate_uuid(idgen.deterministic_int(seed or str(i), 0, 2**63)))
39
+ elif kind == "mid":
40
+ out.append(idgen.generate_mid("CBPR", args.country or "GB", i + 1))
41
+ elif kind == "text":
42
+ out.append(idgen.deterministic_string(seed or "text", 16))
43
+ return out
44
+
45
+
22
46
  def _build_parser() -> argparse.ArgumentParser:
23
47
  p = argparse.ArgumentParser(
24
48
  prog="cbpr-validate",
@@ -72,6 +96,17 @@ def _build_parser() -> argparse.ArgumentParser:
72
96
  help="Root element tag wrapping the AppHdr+Document in --example output "
73
97
  "(default: Envelope).",
74
98
  )
99
+ p.add_argument(
100
+ "--generate",
101
+ choices=("iban", "lei", "bic", "uuid", "uetr", "mid", "text"),
102
+ help="Generate a deterministic, structurally-valid identifier and print it "
103
+ "(use --seed/--country/--bank/--branch/--count to control output).",
104
+ )
105
+ p.add_argument("--seed", default="", help="Seed for --generate (same seed -> same output).")
106
+ p.add_argument("--country", help="Country code for --generate iban/bic/mid (e.g. AT, GB).")
107
+ p.add_argument("--bank", help="Bank code for --generate bic (4 letters, padded with X).")
108
+ p.add_argument("--branch", default="XXX", help="Branch code for --generate bic (default XXX).")
109
+ p.add_argument("--count", type=int, default=1, help="How many ids to generate (default 1).")
75
110
  p.add_argument(
76
111
  "--list",
77
112
  action="store_true",
@@ -211,6 +246,15 @@ def main(argv: Optional[List[str]] = None) -> int:
211
246
  print(json.dumps(types) if args.json else "\n".join(types))
212
247
  return 0
213
248
 
249
+ if args.generate:
250
+ try:
251
+ for line in _generate(args):
252
+ print(line)
253
+ except (ValueError, ValidationError) as exc:
254
+ print(f"error: {exc}", file=sys.stderr)
255
+ return 2
256
+ return 0
257
+
214
258
  if args.example:
215
259
  if args.year is None or args.msgtype is None:
216
260
  print("error: --example requires --year and --type", file=sys.stderr)
@@ -226,13 +270,17 @@ def main(argv: Optional[List[str]] = None) -> int:
226
270
  if args.year is None or args.msgtype is None:
227
271
  print("error: --year and --type are required for --list", file=sys.stderr)
228
272
  return 2
229
- rules = list_rules(args.year, args.msgtype, enforced_only=args.enforced)
273
+ rules = list_rules(
274
+ args.year, args.msgtype, enforced_only=args.enforced, with_xpaths=True
275
+ )
230
276
  if args.json:
231
277
  print(json.dumps(rules, indent=2))
232
278
  else:
233
279
  for r in rules:
234
280
  flag = "" if r["enforced"] else " (advisory)"
235
281
  print(f"{r['rule_number']} - {r['name']}{flag}")
282
+ for xp in r.get("xpaths", []):
283
+ print(f" {xp}")
236
284
  return 0
237
285
 
238
286
  if args.year is None:
@@ -95,15 +95,64 @@ def validate_string(xml: str, year: int, msgtype: Optional[str] = None, xsd=None
95
95
  return _validate_tree(tree, year, msgtype, xsd)
96
96
 
97
97
 
98
- def list_rules(year: int, msgtype: str, enforced_only: bool = False) -> List[dict]:
98
+ def _drop_redundant_bare_paths(paths: set) -> List[str]:
99
+ """Tidy captured paths: drop a bare element path when an ``@attribute`` variant
100
+ exists, and a ``//Name`` wildcard when concrete paths for that name exist."""
101
+ attr_bases = {p.rsplit("/@", 1)[0] for p in paths if "/@" in p}
102
+ wild_names = {p[2:] for p in paths if p.startswith("//")}
103
+ concrete = {p.rsplit("/", 1)[-1].lstrip("@") for p in paths if not p.startswith("//")}
104
+ kept = set()
105
+ for p in paths:
106
+ if p.startswith("//") and p[2:] in concrete:
107
+ continue
108
+ if "/@" not in p and p in attr_bases:
109
+ continue
110
+ kept.add(p)
111
+ return sorted(kept)
112
+
113
+
114
+ def rule_xpaths(year: int, msgtype: str) -> dict:
115
+ """Map each rule_number to the concrete xpaths it touches.
116
+
117
+ Derived by running each rule against the bundled min+max example messages
118
+ and recording the fields its queries read (best-effort; a field absent from
119
+ both examples and read only inside a non-firing branch may be missed).
120
+ """
121
+ rules = load_rules(year, msgtype)
122
+ captured = {r.rule_number: set() for r in rules}
123
+ for variant in ("min", "max"):
124
+ try:
125
+ xml = example_message(year, msgtype, variant)
126
+ tree = loader.parse_string(xml)
127
+ except Exception:
128
+ continue
129
+ bah, doc = loader.locate(tree)
130
+ msg = ParsedMessage(tree, bah, doc, message_type=msgtype, year=year)
131
+ for r in rules:
132
+ if not r.enforced:
133
+ continue
134
+ with msg.record() as paths:
135
+ r.run(msg)
136
+ captured[r.rule_number] |= paths
137
+ return {num: _drop_redundant_bare_paths(p) for num, p in captured.items()}
138
+
139
+
140
+ def list_rules(
141
+ year: int, msgtype: str, enforced_only: bool = False, with_xpaths: bool = False
142
+ ) -> List[dict]:
99
143
  """Return metadata for the rules registered for a (year, message type).
100
144
 
101
145
  With ``enforced_only=True`` only the enforceable (mechanically-checked) rules
102
- are returned, omitting advisory/catalogue-only ones.
146
+ are returned. With ``with_xpaths=True`` each rule dict gains an ``"xpaths"``
147
+ list of the concrete fields it affects (see ``rule_xpaths``).
103
148
  """
104
149
  rules = [r.to_dict() for r in load_rules(year, msgtype)]
105
150
  if enforced_only:
106
151
  rules = [r for r in rules if r["enforced"]]
152
+ if with_xpaths:
153
+ paths = rule_xpaths(year, msgtype)
154
+ for r in rules:
155
+ r["xpaths"] = paths.get(r["rule_number"], [])
107
156
  return rules
108
157
 
109
158