cbpr-usage-rules 0.1.3__tar.gz → 0.3.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 (97) hide show
  1. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/CHANGELOG.md +18 -0
  2. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/MANIFEST.in +1 -0
  3. {cbpr_usage_rules-0.1.3/src/cbpr_usage_rules.egg-info → cbpr_usage_rules-0.3.0}/PKG-INFO +49 -3
  4. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/README.md +48 -2
  5. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/pyproject.toml +1 -1
  6. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/__init__.py +11 -2
  7. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/cli.py +87 -3
  8. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/engine.py +48 -0
  9. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.052.max.xml +3430 -0
  10. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.052.min.xml +35 -0
  11. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.054.max.xml +3334 -0
  12. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/camt.054.min.xml +40 -0
  13. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.002.max.xml +420 -0
  14. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.002.min.xml +41 -0
  15. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.004.max.xml +1731 -0
  16. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.004.min.xml +60 -0
  17. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008.max.xml +1318 -0
  18. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008.min.xml +58 -0
  19. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008_stp.max.xml +1092 -0
  20. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.008_stp.min.xml +67 -0
  21. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009.max.xml +954 -0
  22. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009.min.xml +51 -0
  23. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_adv.max.xml +1036 -0
  24. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_adv.min.xml +51 -0
  25. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_cov.max.xml +1619 -0
  26. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pacs.009_cov.min.xml +65 -0
  27. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pain.001.max.xml +1215 -0
  28. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2025/pain.001.min.xml +50 -0
  29. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.052.max.xml +3430 -0
  30. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.052.min.xml +31 -0
  31. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.054.max.xml +3334 -0
  32. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/camt.054.min.xml +40 -0
  33. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.002.max.xml +420 -0
  34. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.002.min.xml +41 -0
  35. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.004.max.xml +1491 -0
  36. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.004.min.xml +62 -0
  37. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008.max.xml +1295 -0
  38. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008.min.xml +59 -0
  39. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008_stp.max.xml +1072 -0
  40. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.008_stp.min.xml +68 -0
  41. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009.max.xml +954 -0
  42. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009.min.xml +51 -0
  43. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_adv.max.xml +1036 -0
  44. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_adv.min.xml +51 -0
  45. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_cov.max.xml +1616 -0
  46. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pacs.009_cov.min.xml +66 -0
  47. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pain.001.max.xml +1149 -0
  48. cbpr_usage_rules-0.3.0/src/cbpr_rules/examples/y2026/pain.001.min.xml +50 -0
  49. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0/src/cbpr_usage_rules.egg-info}/PKG-INFO +49 -3
  50. cbpr_usage_rules-0.3.0/src/cbpr_usage_rules.egg-info/SOURCES.txt +94 -0
  51. cbpr_usage_rules-0.3.0/tests/test_examples.py +90 -0
  52. cbpr_usage_rules-0.1.3/src/cbpr_usage_rules.egg-info/SOURCES.txt +0 -53
  53. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/LICENSE +0 -0
  54. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/setup.cfg +0 -0
  55. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/helpers.py +0 -0
  56. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/loader.py +0 -0
  57. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/message.py +0 -0
  58. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/models.py +0 -0
  59. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/py.typed +0 -0
  60. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/reference/__init__.py +0 -0
  61. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/reference/countries.py +0 -0
  62. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/reference/currencies.py +0 -0
  63. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/registry.py +0 -0
  64. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/__init__.py +0 -0
  65. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/__init__.py +0 -0
  66. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/camt_052.py +0 -0
  67. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/camt_054.py +0 -0
  68. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_002.py +0 -0
  69. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_004.py +0 -0
  70. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_008.py +0 -0
  71. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_008_stp.py +0 -0
  72. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_009.py +0 -0
  73. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_009_adv.py +0 -0
  74. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pacs_009_cov.py +0 -0
  75. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2025/pain_001.py +0 -0
  76. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/__init__.py +0 -0
  77. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/camt_052.py +0 -0
  78. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/camt_054.py +0 -0
  79. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_002.py +0 -0
  80. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_004.py +0 -0
  81. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_008.py +0 -0
  82. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_008_stp.py +0 -0
  83. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_009.py +0 -0
  84. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_009_adv.py +0 -0
  85. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pacs_009_cov.py +0 -0
  86. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/rules/y2026/pain_001.py +0 -0
  87. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/schema.py +0 -0
  88. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/__init__.py +0 -0
  89. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/bic.py +0 -0
  90. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/country.py +0 -0
  91. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/currency.py +0 -0
  92. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/iban.py +0 -0
  93. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_rules/validators/lei.py +0 -0
  94. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/dependency_links.txt +0 -0
  95. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/entry_points.txt +0 -0
  96. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/requires.txt +0 -0
  97. {cbpr_usage_rules-0.1.3 → cbpr_usage_rules-0.3.0}/src/cbpr_usage_rules.egg-info/top_level.txt +0 -0
@@ -4,6 +4,24 @@ 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.3.0]
8
+
9
+ ### Added
10
+ - Bundled **example messages**: a minimum and a maximum variant for every
11
+ supported message type, each pre-verified to pass the usage rules and the
12
+ CBPR+ XSDs. New API `example_message(year, msgtype, variant="max",
13
+ wrapper="Envelope")` and `example_variants(year, msgtype)`, and CLI
14
+ `--example {min,max}` (with `--wrapper TAG`). Data is fictitious/anonymised.
15
+
16
+ ## [0.2.0]
17
+
18
+ ### Added
19
+ - `--format {text,compact,json}` CLI option. The new `compact` format prints one
20
+ finding per line, compiler/linter-style (`file:line: severity [rule] message | at
21
+ xpath`) with a stable `VALID:`/`INVALID:` summary line — easy for tools and coding
22
+ agents to parse. `--json` is now an alias for `--format json`; the default rich
23
+ text output is unchanged.
24
+
7
25
  ## [0.1.3]
8
26
 
9
27
  ### Changed
@@ -2,3 +2,4 @@ include LICENSE
2
2
  include README.md
3
3
  include CHANGELOG.md
4
4
  recursive-include src/cbpr_rules py.typed
5
+ recursive-include src/cbpr_rules/examples *.xml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cbpr-usage-rules
3
- Version: 0.1.3
3
+ Version: 0.3.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
@@ -112,8 +112,11 @@ result = cbpr_rules.validate_string(xml_text, year=2026)
112
112
  # Human-readable (default). Exit code is non-zero if there are violations.
113
113
  cbpr-validate payment.xml --year 2025
114
114
 
115
- # JSON output
116
- cbpr-validate payment.xml --year 2025 --json
115
+ # Compact, one finding per line (compiler-style) — for tools and coding agents
116
+ cbpr-validate payment.xml --year 2025 --format compact
117
+
118
+ # JSON output (full machine-readable result)
119
+ cbpr-validate payment.xml --year 2025 --json # same as --format json
117
120
 
118
121
  # Read from stdin
119
122
  cat payment.xml | cbpr-validate --year 2026
@@ -127,10 +130,53 @@ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
127
130
 
128
131
  Each reported violation shows the rule, **why** this instance failed (`Problem:`),
129
132
  the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
133
+
134
+ **For tools and coding agents**, prefer `--format compact` (one finding per line,
135
+ parseable like a compiler/linter) or `--json` (the full structured result):
136
+
137
+ ```text
138
+ $ cbpr-validate payment.xml --year 2025 --format compact
139
+ payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
140
+ payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
141
+ INVALID: 5 violations (87 rules)
142
+ ```
143
+
144
+ Each line is `file:line: severity [rule] message | at xpath`, and the final
145
+ `VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
146
+ errors appear as `schema-error` lines and are counted in the summary.
130
147
  Advisory rules are summarised as a count by default; pass `--advisory` to list them.
131
148
 
132
149
  ---
133
150
 
151
+ ## Example messages
152
+
153
+ For every supported message type the package bundles a **minimum** and a
154
+ **maximum** example message — both pre-verified to pass the usage rules *and* the
155
+ CBPR+ XSDs:
156
+
157
+ - **min** — only the mandatory fields (smallest valid message);
158
+ - **max** — every XSD-visible field populated (one representative occurrence; the
159
+ richest single legal combination, since some fields are mutually exclusive).
160
+
161
+ ```bash
162
+ # Print to stdout (full AppHdr + Document, wrapped in <Envelope>)
163
+ cbpr-validate --example max --year 2025 --type pacs.008
164
+ cbpr-validate --example min --year 2026 --type pacs.009_cov > template.xml
165
+
166
+ # Override the wrapper tag (default: Envelope)
167
+ cbpr-validate --example max --year 2025 --type pacs.008 --wrapper BusinessMessage
168
+ ```
169
+ ```python
170
+ import cbpr_rules
171
+ xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults to "max"
172
+ cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
173
+ ```
174
+
175
+ The data is **fictitious** (synthetic BICs/IBANs/LEIs, `EXAMPLE` names) and does
176
+ not reproduce any real sample — use them as starting templates, not real payments.
177
+
178
+ ---
179
+
134
180
  ## The result object
135
181
 
136
182
  Both `validate_file` and `validate_string` return a dictionary:
@@ -57,8 +57,11 @@ result = cbpr_rules.validate_string(xml_text, year=2026)
57
57
  # Human-readable (default). Exit code is non-zero if there are violations.
58
58
  cbpr-validate payment.xml --year 2025
59
59
 
60
- # JSON output
61
- cbpr-validate payment.xml --year 2025 --json
60
+ # Compact, one finding per line (compiler-style) — for tools and coding agents
61
+ cbpr-validate payment.xml --year 2025 --format compact
62
+
63
+ # JSON output (full machine-readable result)
64
+ cbpr-validate payment.xml --year 2025 --json # same as --format json
62
65
 
63
66
  # Read from stdin
64
67
  cat payment.xml | cbpr-validate --year 2026
@@ -72,10 +75,53 @@ cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
72
75
 
73
76
  Each reported violation shows the rule, **why** this instance failed (`Problem:`),
74
77
  the **offending XML** from your file (`Found:`), and its line + xpath (`At:`).
78
+
79
+ **For tools and coding agents**, prefer `--format compact` (one finding per line,
80
+ parseable like a compiler/linter) or `--json` (the full structured result):
81
+
82
+ ```text
83
+ $ cbpr-validate payment.xml --year 2025 --format compact
84
+ payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
85
+ payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
86
+ INVALID: 5 violations (87 rules)
87
+ ```
88
+
89
+ Each line is `file:line: severity [rule] message | at xpath`, and the final
90
+ `VALID:` / `INVALID:` line is a stable summary to grep for. With `--xsd`, schema
91
+ errors appear as `schema-error` lines and are counted in the summary.
75
92
  Advisory rules are summarised as a count by default; pass `--advisory` to list them.
76
93
 
77
94
  ---
78
95
 
96
+ ## Example messages
97
+
98
+ For every supported message type the package bundles a **minimum** and a
99
+ **maximum** example message — both pre-verified to pass the usage rules *and* the
100
+ CBPR+ XSDs:
101
+
102
+ - **min** — only the mandatory fields (smallest valid message);
103
+ - **max** — every XSD-visible field populated (one representative occurrence; the
104
+ richest single legal combination, since some fields are mutually exclusive).
105
+
106
+ ```bash
107
+ # Print to stdout (full AppHdr + Document, wrapped in <Envelope>)
108
+ cbpr-validate --example max --year 2025 --type pacs.008
109
+ cbpr-validate --example min --year 2026 --type pacs.009_cov > template.xml
110
+
111
+ # Override the wrapper tag (default: Envelope)
112
+ cbpr-validate --example max --year 2025 --type pacs.008 --wrapper BusinessMessage
113
+ ```
114
+ ```python
115
+ import cbpr_rules
116
+ xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults to "max"
117
+ cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
118
+ ```
119
+
120
+ The data is **fictitious** (synthetic BICs/IBANs/LEIs, `EXAMPLE` names) and does
121
+ not reproduce any real sample — use them as starting templates, not real payments.
122
+
123
+ ---
124
+
79
125
  ## The result object
80
126
 
81
127
  Both `validate_file` and `validate_string` return a dictionary:
@@ -50,7 +50,7 @@ version = { attr = "cbpr_rules.__version__" }
50
50
  where = ["src"]
51
51
 
52
52
  [tool.setuptools.package-data]
53
- cbpr_rules = ["py.typed"]
53
+ cbpr_rules = ["py.typed", "examples/**/*.xml"]
54
54
 
55
55
  [tool.pytest.ini_options]
56
56
  testpaths = ["tests"]
@@ -4,10 +4,17 @@ 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.1.3"
7
+ __version__ = "0.3.0"
8
8
 
9
9
  from .models import Rule, Severity, Violation
10
- from .engine import validate_file, validate_string, list_rules, available
10
+ from .engine import (
11
+ validate_file,
12
+ validate_string,
13
+ list_rules,
14
+ available,
15
+ example_message,
16
+ example_variants,
17
+ )
11
18
 
12
19
  __all__ = [
13
20
  "__version__",
@@ -18,4 +25,6 @@ __all__ = [
18
25
  "validate_string",
19
26
  "list_rules",
20
27
  "available",
28
+ "example_message",
29
+ "example_variants",
21
30
  ]
@@ -9,7 +9,14 @@ import textwrap
9
9
  from typing import List, Optional
10
10
 
11
11
  from . import __version__
12
- from .engine import ValidationError, available, list_rules, validate_file, validate_string
12
+ from .engine import (
13
+ ValidationError,
14
+ available,
15
+ example_message,
16
+ list_rules,
17
+ validate_file,
18
+ validate_string,
19
+ )
13
20
 
14
21
 
15
22
  def _build_parser() -> argparse.ArgumentParser:
@@ -26,7 +33,18 @@ def _build_parser() -> argparse.ArgumentParser:
26
33
  help="Message type override, e.g. pacs.008 or pacs.008_stp "
27
34
  "(auto-detected from the Document namespace if omitted).",
28
35
  )
29
- p.add_argument("--json", action="store_true", help="Emit JSON instead of text.")
36
+ p.add_argument(
37
+ "--format",
38
+ choices=("text", "compact", "json"),
39
+ default="text",
40
+ help="Output format: text (rich, default), compact (one finding per line, "
41
+ "compiler-style — for tools/agents), or json (full machine-readable).",
42
+ )
43
+ p.add_argument(
44
+ "--json",
45
+ action="store_true",
46
+ help="Alias for --format json.",
47
+ )
30
48
  p.add_argument(
31
49
  "--xsd",
32
50
  action="append",
@@ -40,6 +58,20 @@ def _build_parser() -> argparse.ArgumentParser:
40
58
  action="store_true",
41
59
  help="Also list the advisory (non-enforced) rules in full.",
42
60
  )
61
+ p.add_argument(
62
+ "--example",
63
+ choices=("min", "max"),
64
+ help="Print a bundled example message for --year/--type instead of validating: "
65
+ "min (mandatory fields only) or max (every field populated). Both are XSD- and "
66
+ "usage-rule-valid.",
67
+ )
68
+ p.add_argument(
69
+ "--wrapper",
70
+ default="Envelope",
71
+ metavar="TAG",
72
+ help="Root element tag wrapping the AppHdr+Document in --example output "
73
+ "(default: Envelope).",
74
+ )
43
75
  p.add_argument(
44
76
  "--list",
45
77
  action="store_true",
@@ -125,6 +157,43 @@ def _format_text(result: dict, show_advisory: bool = False) -> str:
125
157
  return "\n".join(lines)
126
158
 
127
159
 
160
+ def _format_compact(result: dict, source: str) -> str:
161
+ """One finding per line, compiler/linter-style, for tools and coding agents.
162
+
163
+ ``<source>:<line>: <severity> [<rule>] <message> | at <xpath>`` per violation,
164
+ followed by a single greppable ``VALID:``/``INVALID:`` summary line. Advisory
165
+ (non-enforced) rules are omitted; use --json or text --advisory for those.
166
+ """
167
+ lines: List[str] = []
168
+ for v in result["violations"]:
169
+ line = v["line"] if v["line"] is not None else "?"
170
+ message = " ".join((v.get("detail") or v["description"] or "").split())
171
+ suffix = f" | at {v['xpath']}" if v.get("xpath") else ""
172
+ lines.append(f"{source}:{line}: {v['severity']} [{v['rule_number']}] {message}{suffix}")
173
+
174
+ xsd = result.get("xsd")
175
+ schema_errors = 0
176
+ if xsd is not None:
177
+ for s in xsd["schemas"]:
178
+ if s.get("load_error"):
179
+ schema_errors += 1
180
+ lines.append(f"{s['file']}: schema-error could not load schema: {s['load_error']}")
181
+ for e in s["errors"]:
182
+ schema_errors += 1
183
+ eline = e["line"] if e.get("line") is not None else "?"
184
+ msg = " ".join((e.get("message") or "").split())
185
+ lines.append(f"{s['file']}:{eline}: schema-error {msg}")
186
+
187
+ n = len(result["violations"])
188
+ verdict = "VALID" if result["valid"] else "INVALID"
189
+ parts = [f"{n} violation{'s' if n != 1 else ''}"]
190
+ if xsd is not None:
191
+ parts.append(f"{schema_errors} schema error{'s' if schema_errors != 1 else ''}")
192
+ summary = f"{verdict}: {', '.join(parts)} ({result['rules_evaluated']} rules)"
193
+ lines.append(summary)
194
+ return "\n".join(lines)
195
+
196
+
128
197
  def main(argv: Optional[List[str]] = None) -> int:
129
198
  args = _build_parser().parse_args(argv)
130
199
 
@@ -136,6 +205,17 @@ def main(argv: Optional[List[str]] = None) -> int:
136
205
  print(json.dumps(types) if args.json else "\n".join(types))
137
206
  return 0
138
207
 
208
+ if args.example:
209
+ if args.year is None or args.msgtype is None:
210
+ print("error: --example requires --year and --type", file=sys.stderr)
211
+ return 2
212
+ try:
213
+ print(example_message(args.year, args.msgtype, args.example, wrapper=args.wrapper))
214
+ except ValidationError as exc:
215
+ print(f"error: {exc}", file=sys.stderr)
216
+ return 2
217
+ return 0
218
+
139
219
  if args.list:
140
220
  if args.year is None or args.msgtype is None:
141
221
  print("error: --year and --type are required for --list", file=sys.stderr)
@@ -153,6 +233,7 @@ def main(argv: Optional[List[str]] = None) -> int:
153
233
  print("error: --year is required to validate", file=sys.stderr)
154
234
  return 2
155
235
 
236
+ source = args.file or "<stdin>"
156
237
  try:
157
238
  if args.file:
158
239
  result = validate_file(args.file, args.year, args.msgtype, xsd=args.xsd)
@@ -163,8 +244,11 @@ def main(argv: Optional[List[str]] = None) -> int:
163
244
  print(f"error: {exc}", file=sys.stderr)
164
245
  return 2
165
246
 
166
- if args.json:
247
+ fmt = "json" if args.json else args.format
248
+ if fmt == "json":
167
249
  print(json.dumps(result, indent=2))
250
+ elif fmt == "compact":
251
+ print(_format_compact(result, source))
168
252
  else:
169
253
  print(_format_text(result, show_advisory=args.advisory))
170
254
 
@@ -1,14 +1,19 @@
1
1
  """Validation engine and public API."""
2
2
  from __future__ import annotations
3
3
 
4
+ from importlib import resources
4
5
  from typing import List, Optional
5
6
 
7
+ from lxml import etree
8
+
6
9
  from . import loader
7
10
  from . import schema as _schema
8
11
  from .message import ParsedMessage
9
12
  from .models import Rule, Severity
10
13
  from .registry import available_message_types, load_rules
11
14
 
15
+ _VARIANTS = ("min", "max")
16
+
12
17
 
13
18
  class ValidationError(Exception):
14
19
  """Raised when a message cannot be validated (parse error, unknown type)."""
@@ -98,3 +103,46 @@ def list_rules(year: int, msgtype: str) -> List[dict]:
98
103
  def available(year: int) -> List[str]:
99
104
  """Message types with rules available for a given year."""
100
105
  return available_message_types(year)
106
+
107
+
108
+ def _example_resource(year: int, msgtype: str, variant: str):
109
+ return (
110
+ resources.files("cbpr_rules")
111
+ .joinpath("examples", f"y{int(year)}", f"{msgtype}.{variant}.xml")
112
+ )
113
+
114
+
115
+ def example_variants(year: int, msgtype: str) -> List[str]:
116
+ """Which example variants ('min'/'max') are bundled for a (year, message type)."""
117
+ return [v for v in _VARIANTS if _example_resource(year, msgtype, v).is_file()]
118
+
119
+
120
+ def example_message(
121
+ year: int, msgtype: str, variant: str = "max", wrapper: str = "Envelope"
122
+ ) -> str:
123
+ """Return a bundled, pre-verified example message for a (year, message type).
124
+
125
+ ``variant`` is 'min' (mandatory fields only) or 'max' (every field populated).
126
+ Both are guaranteed to pass the usage rules (and the CBPR+ XSDs). ``wrapper``
127
+ overrides the single root element tag (default ``Envelope``) that holds the
128
+ ``AppHdr`` and ``Document``. Raises ``ValidationError`` if no such example.
129
+ """
130
+ if variant not in _VARIANTS:
131
+ raise ValidationError(
132
+ f"Unknown variant '{variant}'; choose one of {', '.join(_VARIANTS)}."
133
+ )
134
+ res = _example_resource(year, msgtype, variant)
135
+ if not res.is_file():
136
+ types = ", ".join(available(year)) or "(none)"
137
+ raise ValidationError(
138
+ f"No {variant} example for '{msgtype}' in {int(year)}. "
139
+ f"Available message types: {types}."
140
+ )
141
+ text = res.read_text(encoding="utf-8")
142
+ if wrapper and wrapper != "Envelope":
143
+ root = etree.fromstring(text.encode("utf-8"))
144
+ root.tag = wrapper
145
+ text = etree.tostring(
146
+ root, xml_declaration=True, encoding="UTF-8", pretty_print=True
147
+ ).decode("utf-8")
148
+ return text