iso-tollgate 0.1.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 (56) hide show
  1. iso_tollgate-0.1.0/.github/actions/validate/action.yml +54 -0
  2. iso_tollgate-0.1.0/.github/actions/validate/scripts/run_validation.py +83 -0
  3. iso_tollgate-0.1.0/.github/workflows/example-consumer-usage.yml +26 -0
  4. iso_tollgate-0.1.0/.github/workflows/tests.yml +33 -0
  5. iso_tollgate-0.1.0/.gitignore +24 -0
  6. iso_tollgate-0.1.0/CLAUDE.md +66 -0
  7. iso_tollgate-0.1.0/CONTRIBUTING.md +39 -0
  8. iso_tollgate-0.1.0/LICENSE +201 -0
  9. iso_tollgate-0.1.0/PKG-INFO +109 -0
  10. iso_tollgate-0.1.0/README.md +80 -0
  11. iso_tollgate-0.1.0/TROUBLESHOOTING.md +83 -0
  12. iso_tollgate-0.1.0/docs/EVAL_BASELINE_REPORT.md +72 -0
  13. iso_tollgate-0.1.0/docs/RESEARCH_NOTES.md +136 -0
  14. iso_tollgate-0.1.0/docs/SOURCES.md +388 -0
  15. iso_tollgate-0.1.0/docs/usage.md +130 -0
  16. iso_tollgate-0.1.0/docs/why.md +53 -0
  17. iso_tollgate-0.1.0/pyproject.toml +52 -0
  18. iso_tollgate-0.1.0/src/tollgate/__init__.py +31 -0
  19. iso_tollgate-0.1.0/src/tollgate/api.py +308 -0
  20. iso_tollgate-0.1.0/src/tollgate/cli.py +247 -0
  21. iso_tollgate-0.1.0/src/tollgate/explain/__init__.py +14 -0
  22. iso_tollgate-0.1.0/src/tollgate/explain/explainer.py +83 -0
  23. iso_tollgate-0.1.0/src/tollgate/explain/prompts.py +54 -0
  24. iso_tollgate-0.1.0/src/tollgate/generator/__init__.py +8 -0
  25. iso_tollgate-0.1.0/src/tollgate/generator/synthetic_fixtures.py +504 -0
  26. iso_tollgate-0.1.0/src/tollgate/report/__init__.py +1 -0
  27. iso_tollgate-0.1.0/src/tollgate/report/markdown_report.py +87 -0
  28. iso_tollgate-0.1.0/src/tollgate/schemas/pacs.008.001.08.xsd +1125 -0
  29. iso_tollgate-0.1.0/src/tollgate/validation/__init__.py +3 -0
  30. iso_tollgate-0.1.0/src/tollgate/validation/address_rule.py +256 -0
  31. iso_tollgate-0.1.0/src/tollgate/validation/charset_rule.py +116 -0
  32. iso_tollgate-0.1.0/src/tollgate/validation/currency_rule.py +154 -0
  33. iso_tollgate-0.1.0/src/tollgate/validation/mandatory_gap_rule.py +120 -0
  34. iso_tollgate-0.1.0/src/tollgate/validation/models.py +37 -0
  35. iso_tollgate-0.1.0/src/tollgate/validation/truncation_rule.py +139 -0
  36. iso_tollgate-0.1.0/src/tollgate/validation/xsd_validator.py +168 -0
  37. iso_tollgate-0.1.0/tests/__init__.py +0 -0
  38. iso_tollgate-0.1.0/tests/evals/eval_harness.py +284 -0
  39. iso_tollgate-0.1.0/tests/evals/eval_results/.gitkeep +0 -0
  40. iso_tollgate-0.1.0/tests/fixtures/.gitkeep +0 -0
  41. iso_tollgate-0.1.0/tests/test_address_rule.py +137 -0
  42. iso_tollgate-0.1.0/tests/test_api.py +137 -0
  43. iso_tollgate-0.1.0/tests/test_batch_checking.py +129 -0
  44. iso_tollgate-0.1.0/tests/test_charset_rule.py +113 -0
  45. iso_tollgate-0.1.0/tests/test_cli.py +247 -0
  46. iso_tollgate-0.1.0/tests/test_currency_rule.py +188 -0
  47. iso_tollgate-0.1.0/tests/test_data_handling.py +126 -0
  48. iso_tollgate-0.1.0/tests/test_edge_cases.py +185 -0
  49. iso_tollgate-0.1.0/tests/test_eval_harness.py +183 -0
  50. iso_tollgate-0.1.0/tests/test_explainer.py +134 -0
  51. iso_tollgate-0.1.0/tests/test_github_action_script.py +116 -0
  52. iso_tollgate-0.1.0/tests/test_inject_error.py +146 -0
  53. iso_tollgate-0.1.0/tests/test_mandatory_gap_rule.py +84 -0
  54. iso_tollgate-0.1.0/tests/test_performance.py +77 -0
  55. iso_tollgate-0.1.0/tests/test_truncation_rule.py +114 -0
  56. iso_tollgate-0.1.0/tests/test_xsd_validator.py +103 -0
@@ -0,0 +1,54 @@
1
+ name: "Tollgate ISO 20022 Validator"
2
+ description: >
3
+ Validates pacs.008 ISO 20022 payment files in CI, catching the gap
4
+ between schema-valid and network-acceptable before they're merged or
5
+ deployed. Fails the build on any error-severity finding.
6
+ author: "Arun"
7
+ branding:
8
+ icon: "shield"
9
+ color: "blue"
10
+
11
+ inputs:
12
+ path:
13
+ description: >
14
+ Glob pattern or path to the pacs.008 XML file(s) to validate.
15
+ Supports a single file or a glob (e.g. "payments/**/*.xml").
16
+ required: true
17
+ fail-on-warning:
18
+ description: >
19
+ If "true", treat warning-severity findings (e.g. truncation
20
+ heuristics) as build failures too, not just errors. Default
21
+ "false" -- warnings are heuristic signals, not certain
22
+ failures, per Tollgate's own severity design; most CI pipelines
23
+ should not hard-fail on a heuristic.
24
+ required: false
25
+ default: "false"
26
+ python-version:
27
+ description: "Python version to set up for running Tollgate."
28
+ required: false
29
+ default: "3.11"
30
+
31
+ outputs:
32
+ has-errors:
33
+ description: "true if any error-severity violation was found across all checked files."
34
+ value: ${{ steps.run-tollgate.outputs.has-errors }}
35
+ results-json:
36
+ description: "Combined JSON results for all checked files."
37
+ value: ${{ steps.run-tollgate.outputs.results-json }}
38
+
39
+ runs:
40
+ using: "composite"
41
+ steps:
42
+ - name: Set up Python
43
+ uses: actions/setup-python@v5
44
+ with:
45
+ python-version: ${{ inputs.python-version }}
46
+
47
+ - name: Install Tollgate
48
+ shell: bash
49
+ run: pip install iso-tollgate
50
+
51
+ - name: Run Tollgate against matched files
52
+ id: run-tollgate
53
+ shell: bash
54
+ run: python3 "${{ github.action_path }}/scripts/run_validation.py" "${{ inputs.path }}" "${{ inputs.fail-on-warning }}"
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """Helper script for the Tollgate GitHub Action.
3
+
4
+ Pulled out of action.yml deliberately: embedding multi-line Python
5
+ inside a bash heredoc inside a YAML block scalar is fragile and hard
6
+ to read -- a real YAML syntax error was found in an earlier draft of
7
+ action.yml caused by exactly this nesting. A standalone script is
8
+ easier to test, easier to read, and avoids YAML/bash/Python
9
+ quoting interactions entirely.
10
+
11
+ Usage:
12
+ python3 run_validation.py "<glob-pattern>" <fail-on-warning: true|false>
13
+
14
+ Exits 1 if any file has an error-severity violation (or a warning,
15
+ if fail-on-warning is true). Prints the combined results as JSON to
16
+ stdout, and writes outputs to $GITHUB_OUTPUT if that env var is set.
17
+ """
18
+
19
+ import glob
20
+ import json
21
+ import os
22
+ import subprocess
23
+ import sys
24
+
25
+
26
+ def main() -> int:
27
+ if len(sys.argv) != 3:
28
+ print("Usage: run_validation.py <glob-pattern> <fail-on-warning>", file=sys.stderr)
29
+ return 1
30
+
31
+ pattern, fail_on_warning_str = sys.argv[1], sys.argv[2]
32
+ fail_on_warning = fail_on_warning_str.strip().lower() == "true"
33
+
34
+ files = sorted(glob.glob(pattern, recursive=True))
35
+ if not files:
36
+ print(f"::error::No files matched path pattern: {pattern}")
37
+ return 1
38
+
39
+ combined_results = []
40
+ overall_has_errors = False
41
+
42
+ for file_path in files:
43
+ print(f"Validating {file_path}...")
44
+ proc = subprocess.run(
45
+ ["tollgate", "validate", file_path, "--json"],
46
+ capture_output=True,
47
+ text=True,
48
+ )
49
+
50
+ try:
51
+ entry = json.loads(proc.stdout)
52
+ except json.JSONDecodeError:
53
+ print(f"::error file={file_path}::Tollgate produced unparseable output: {proc.stdout!r}")
54
+ overall_has_errors = True
55
+ continue
56
+
57
+ entry["file"] = file_path
58
+ combined_results.append(entry)
59
+
60
+ if entry.get("has_errors"):
61
+ print(f"::error file={file_path}::Tollgate found error-severity violation(s)")
62
+ overall_has_errors = True
63
+
64
+ if entry.get("has_warnings") and fail_on_warning:
65
+ print(f"::error file={file_path}::Tollgate found warning-severity finding(s) (fail-on-warning enabled)")
66
+ overall_has_errors = True
67
+
68
+ results_json = json.dumps(combined_results)
69
+
70
+ github_output = os.environ.get("GITHUB_OUTPUT")
71
+ if github_output:
72
+ with open(github_output, "a") as f:
73
+ f.write(f"has-errors={'true' if overall_has_errors else 'false'}\n")
74
+ f.write("results-json<<TOLLGATE_EOF\n")
75
+ f.write(results_json + "\n")
76
+ f.write("TOLLGATE_EOF\n")
77
+
78
+ print(results_json)
79
+ return 1 if overall_has_errors else 0
80
+
81
+
82
+ if __name__ == "__main__":
83
+ sys.exit(main())
@@ -0,0 +1,26 @@
1
+ name: Validate payment files
2
+
3
+ # Example workflow for repos that GENERATE or transform pacs.008 files
4
+ # and want to catch problems before merge/deploy -- not Tollgate's own
5
+ # CI (Tollgate's own tests run via pytest directly, see below).
6
+ #
7
+ # Copy this file into .github/workflows/ in YOUR repo, adjust the
8
+ # `path` glob to match where your pacs.008 files live, and remove the
9
+ # parts referencing this being an example.
10
+
11
+ on:
12
+ pull_request:
13
+ paths:
14
+ - "payments/**/*.xml" # adjust to your repo's actual file locations
15
+
16
+ jobs:
17
+ validate-payments:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Validate pacs.008 files with Tollgate
23
+ uses: iso-tollgate/tollgate/.github/actions/validate@main
24
+ with:
25
+ path: "payments/**/*.xml"
26
+ fail-on-warning: "false"
@@ -0,0 +1,33 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Python ${{ matrix.python-version }}
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+
22
+ - name: Install Tollgate with dev dependencies
23
+ run: pip install -e ".[dev]"
24
+
25
+ - name: Run tests
26
+ # No ANTHROPIC_API_KEY in CI -- the three live-API tests in
27
+ # test_explainer.py skip automatically without one (see their
28
+ # skipif markers). This means CI verifies every deterministic
29
+ # rule, the generator, the eval harness's scoring logic, and
30
+ # the CLI/library API -- everything except the one part that
31
+ # genuinely requires a paid, live model call. That's a
32
+ # deliberate, documented gap, not an oversight.
33
+ run: pytest tests/ -v
@@ -0,0 +1,24 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+
10
+ .pytest_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ .env
15
+ *.env.local
16
+
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+
21
+ tests/evals/eval_results/*.json
22
+ !tests/evals/eval_results/.gitkeep
23
+
24
+ .DS_Store
@@ -0,0 +1,66 @@
1
+ # CLAUDE.md
2
+
3
+ Read this before doing any work in this repo. It encodes hard-won lessons from building Tollgate, not aspirational rules — every item here exists because skipping it caused a real, found bug.
4
+
5
+ ## What this project is
6
+
7
+ A pre-submission safety gate for ISO 20022 pacs.008 payment messages. Catches messages that pass XSD schema validation but would still be rejected (or silently misinterpreted) by a real payment network. v1 scope is pacs.008.001.08 only — do not add a second message type without an explicit decision to do so.
8
+
9
+ ## The core discipline: research and verify before writing code
10
+
11
+ Every validation rule in this project traces to a primary or clearly-identified source — see `docs/SOURCES.md`. Before adding a new rule:
12
+
13
+ 1. Research the gotcha properly. Don't guess at field names, currency lists, or thresholds — find a real source (regulator documentation, the standard's own publisher, official schema definitions).
14
+ 2. Verify the claim against the actual vendored XSD (`src/tollgate/schemas/pacs.008.001.08.xsd`) before writing detection logic. Multiple sessions found that secondary sources (blog posts, even careful research notes) contained claims that didn't hold up against the real schema — the FAIM-tag claim in `mandatory_gap_rule.py`'s history is the canonical example; it was replaced with a fully-verified UETR finding instead of shipping an unconfirmed citation.
15
+ 3. If a primary source can't be directly verified (e.g. blocked by network access, paywalled), say so explicitly in the code and in `SOURCES.md` rather than asserting confidence you don't have. See the currency_rule.py honest-limitation note for the pattern.
16
+ 4. Write the rule, then **test it against deliberately adversarial input before trusting it** — not just the happy path. Every one of this project's six rules had at least one real bug found this way:
17
+ - `charset_rule.py`: the original character-set regex was missing a plain space character, meaning it would have flagged ordinary text like "Tomas Becker" as a violation.
18
+ - `truncation_rule.py`: a naive version would have flagged any field at exactly 35/70 chars, including fields whose own legitimate maximum IS 35/70 — caught by reasoning through the design before writing code, not by a failing test.
19
+ - `address_rule.py`: agent roles (DbtrAgt, etc.) nest their address one level deeper than party roles — a naive `role_tag/PstlAdr` search would have silently missed every agent-role violation.
20
+ - `currency_rule.py`: testing it against the generator's own clean baseline output surfaced a real, pre-existing generator bug (JPY amounts always formatted with 2 decimal places, when JPY supports 0) that had been silently wrong since the project's first session.
21
+ - `xsd_validator.py`: the exception handler let a raw Python stack trace reach the user for non-XML input, and separately, leaked a local filesystem path into an error message for a different malformed-input case.
22
+
23
+ If you find yourself trusting a docstring's claim ("X is handled," "Y is verified") without running code to confirm it — stop and verify first. This has been wrong multiple times in this project's history.
24
+
25
+ ## Severity discipline
26
+
27
+ - `severity="error"`: a deterministic, schema-level-confident violation (XSD failure, character set violation, address structure violation, missing network-mandatory field).
28
+ - `severity="warning"`: a heuristic signal, not a certainty (truncation suspicion, currency decimal mismatch where the failure mode is "might be silently misinterpreted" rather than "will be rejected"). Never upgrade a warning to an error just to make output look more decisive — the uncertainty is real and the explanation layer needs to communicate it honestly.
29
+
30
+ ## The deterministic/AI split — do not blur this
31
+
32
+ Validation logic (does X violate a rule) is deterministic Python, never an LLM call. The AI layer (`explain/explainer.py`) only narrates an already-detected violation in plain English — it never decides whether something is wrong. `--explain` is opt-in (flag), not default, because it's a real billed API call; the five-then-six deterministic checks are free and local.
33
+
34
+ **Data handling, non-negotiable:** `Violation.raw_value` (which can contain a real name, address, or other sensitive field content) must never be sent to the Anthropic API. It's fine in local output (CLI report, JSON, markdown) — the restriction is specifically about the network boundary. See `docs/SOURCES.md#data-handling-ai-boundary` and `tests/test_data_handling.py` for the enforced/tested version of this rule. If you're touching `explain/prompts.py` or `explain/explainer.py`, re-read this before changing what gets sent.
35
+
36
+ ## Adding a new validation rule: the checklist
37
+
38
+ 1. Research + cite in `docs/SOURCES.md` first.
39
+ 2. Verify against the real vendored XSD before writing detection code.
40
+ 3. Add a `RuleId` enum value in `validation/models.py`.
41
+ 4. Write the rule module (`validation/<name>_rule.py`), following the existing pattern: a `_local_path()` helper for readable field paths, walk-by-structural-property (attribute presence, tag name, or tree depth) rather than assuming a fixed shape until you've checked the schema.
42
+ 5. Write a real injector in `generator/synthetic_fixtures.py`'s `_INJECTORS` dict — every `RuleId` needs one, or `tollgate generate` and the eval harness will break for that rule (this has happened — adding a RuleId without wiring it into every consumer is a real, recurring integration gap).
43
+ 6. Wire the new check into `api.py`'s `_run_all_checks()` — this is the actual source of truth the CLI and library both call into.
44
+ 7. Wire it into the eval harness (`tests/evals/eval_harness.py`): `RULE_ID_SYNONYMS`, `DETECTOR_FOR_RULE`, `_run_detector_for_rule`.
45
+ 8. Write tests proving: (a) a clean baseline has zero violations across several seeds, (b) the showcase case (schema-valid, rule-invalid) with an actual XSD validation run alongside it to prove the gap, (c) no false positive on a legitimate edge case that resembles the violation but isn't one.
46
+ 9. Run the FULL test suite, not just the new file — adding a RuleId touches shared enums and dicts that other tests assert against.
47
+
48
+ ## Testing conventions
49
+
50
+ - Always `cp -r source/. dest/` (trailing `/.`) when copying the repo for a clean test environment — `cp -r source/* dest/` silently skips dotfiles/dotdirs (`.github/`, `.gitignore`), which has caused confusion mid-session before.
51
+ - `pip install -e ".[dev]"` then `pytest tests/` for the full suite.
52
+ - `tests/test_explainer.py` has 3 tests gated by `ANTHROPIC_API_KEY` — they skip cleanly without one. Don't treat a skip as a failure; don't treat a skip as "verified" either. As of 2026-06-21 these have been live-verified once on a real machine with a real key — if you change `explainer.py` or `prompts.py`, re-run them for real before trusting the change.
53
+ - On macOS, `pip`/`pytest` may need `python3 -m pip` / `python3 -m pytest`, and a venv is required if you hit "externally-managed-environment" (`python3 -m venv .venv && source .venv/bin/activate`). See `TROUBLESHOOTING.md`.
54
+
55
+ ## Repo layout
56
+
57
+ - `github.com/iso-tollgate/tollgate` — main repo
58
+ - `github.com/iso-tollgate/homebrew-tollgate` — Homebrew tap, scaffolded but not live (needs a real PyPI release with sdist + sha256 first)
59
+ - Not yet published to PyPI as of this writing — see `docs/SOURCES.md` and session history for the dry-run build verification already done (sdist/wheel build correctly, schema file is bundled, `twine check` passes).
60
+
61
+ ## Don't
62
+
63
+ - Don't add a feature "because it'd be nice" without checking it against the original brief's scope discipline (one message type, pre-submission sanity check, not a SWIFT-certified compliance tool).
64
+ - Don't claim a rule is sourced without an actual citation in `docs/SOURCES.md`.
65
+ - Don't trust that "all tests pass" after adding a RuleId without running the FULL suite — shared mappings break silently otherwise.
66
+ - Don't write AI-sounding filler in README/docs. Real examples, real verified command output, no invented case studies.
@@ -0,0 +1,39 @@
1
+ # Contributing
2
+
3
+ Tollgate's credibility rests on one thing: every rule traces to a real source, and every claim has been tested, not assumed. That's not a style preference — it's the whole point of the project. If you're adding a rule, a fix, or a feature, the norms below exist to keep that true.
4
+
5
+ ## The non-negotiable: source every rule
6
+
7
+ If you're adding a new validation rule, it needs a citation in [`docs/SOURCES.md`](docs/SOURCES.md) before it ships — a primary source where possible (a regulator's own documentation, the official XSD, SWIFT's own specifications), or a clearly-identified secondary source with the gap stated honestly if a primary source isn't available.
8
+
9
+ Don't add a rule based on something you half-remember or a claim you found in someone else's blog post without checking it yourself. This project has already found and corrected one rule that was built on an unverified secondhand citation (see `docs/SOURCES.md`'s `fedwire-faim-comparison` entry, kept visible specifically as an example of what *not* to ship) — that correction is part of the project's history on purpose, so the bar stays visible.
10
+
11
+ ## Test against real generated fixtures, not assumptions
12
+
13
+ Every rule module has a corresponding test file that runs the rule against output from `generator/synthetic_fixtures.py` — not hand-written XML strings, not mocked data. If you're fixing a bug, write a regression test that would have caught it, using the same generator.
14
+
15
+ This project has a real track record of bugs found specifically by testing rather than trusting:
16
+ - A character-set rule that flagged completely normal text as a violation because the allowed-character list was missing a plain space
17
+ - An address rule that assumed every party type stored its address at the same nesting depth, which would have silently missed every bank-address violation
18
+ - A truncation rule that would have false-positived on values legitimately using a field's own real maximum length — caught by reasoning through the design before any code was written
19
+ - An AI explanation layer that sent a real person's name to a third-party API, found by checking what data actually left the machine
20
+
21
+ None of these were caught by code review or by the implementation "looking right." They were caught by deliberately running the code against real or adversarial input and checking the actual output. Do the same for anything you add.
22
+
23
+ ## The deterministic-check / AI-narration split is load-bearing
24
+
25
+ Validation logic (does this violate a rule) must be deterministic code — no AI in `validation/*.py`. AI only narrates an *already-detected* violation, in `explain/explainer.py`, via a single API call with no tool use or agentic loop. This split exists so the eval harness can score explanations against known ground truth, and so nothing in this tool can be second-guessed as "is this a real finding or a model's guess." If you're tempted to use AI to *detect* something rather than explain something already detected, that's the wrong layer — open an issue and discuss first.
26
+
27
+ ## Data handling boundary
28
+
29
+ Never send `Violation.raw_value` (or any field content) across the network to a third-party API by default. See `docs/SOURCES.md`'s `data-handling-ai-boundary` section and `tests/test_data_handling.py` for what this means in practice and how it's verified — that test mocks the API client and inspects the actual payload sent, which is the standard to match for any change touching the explain layer.
30
+
31
+ ## Before opening a PR
32
+
33
+ - Run the full test suite: `pytest tests/`. It should pass without an `ANTHROPIC_API_KEY` set — the 3 live-API tests in `test_explainer.py` skip automatically without one; that's expected, not a failure.
34
+ - If you're touching anything in `explain/`, also run with a real `ANTHROPIC_API_KEY` set at least once before merging, since that's the one part of the codebase that can't be fully verified by the deterministic suite alone.
35
+ - If you're adding a rule, confirm it's cited in `docs/SOURCES.md` and that your test file exercises both the violation case and a clean-message case (no false positives).
36
+
37
+ ## Scope
38
+
39
+ v1 covers exactly one message type: pacs.008.001.08. If you want to add a second message type or extend beyond what's documented in [`docs/why.md`](docs/why.md)'s "what it explicitly does not do" section, raise it as an issue first — that's a scope decision, not just a code change.
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the
44
+ purposes of this License, Derivative Works shall not include works
45
+ that remain separable from, or merely link (or bind by name) to the
46
+ interfaces of, the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including the
49
+ original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing
141
+ the origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: iso-tollgate
3
+ Version: 0.1.0
4
+ Summary: Pre-submission safety gate for ISO 20022 payment messages. Catches the gap between schema-valid and network-acceptable before you submit.
5
+ Project-URL: Homepage, https://github.com/iso-tollgate/tollgate
6
+ Project-URL: Repository, https://github.com/iso-tollgate/tollgate
7
+ Project-URL: Issues, https://github.com/iso-tollgate/tollgate/issues
8
+ Project-URL: Documentation, https://github.com/iso-tollgate/tollgate/blob/main/docs/usage.md
9
+ Author: Arun
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: fedwire,iso20022,pacs.008,payments,swift,validation
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Topic :: Office/Business :: Financial
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: anthropic>=0.40
20
+ Requires-Dist: lxml>=5.0
21
+ Requires-Dist: pydantic>=2.0
22
+ Requires-Dist: rich>=13.0
23
+ Requires-Dist: typer>=0.12
24
+ Requires-Dist: xmlschema>=3.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # tollgate
31
+
32
+ **Catches ISO 20022 payment messages that pass schema validation and still get rejected by the network — before you find out the hard way.**
33
+
34
+ [![Tests](https://img.shields.io/badge/tests-163%20passing-brightgreen)](#status) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](pyproject.toml)
35
+
36
+ A pacs.008 payment message can be 100% valid XML, pass every XSD check, and still bounce off a real clearing network — because some of the rules that matter live outside the schema entirely. Tollgate catches that gap.
37
+
38
+ ## Quickstart
39
+
40
+ ```bash
41
+ git clone https://github.com/iso-tollgate/tollgate.git
42
+ cd tollgate
43
+ pip install -e .
44
+ ```
45
+
46
+ ```bash
47
+ tollgate validate payment.xml
48
+ ```
49
+
50
+ ```
51
+ 1 error(s), 0 warning(s) found in payment.xml:
52
+
53
+ ERROR charset_violation -- FIToFICstmrCdtTrf/CdtTrfTxInf/Dbtr/Nm
54
+ Contains character(s) outside SWIFT's character set X: 'ü'. This is
55
+ schema-valid XML (ISO 20022 permits full Unicode) but SWIFT's network
56
+ layer restricts allowed characters independently of the schema --
57
+ this will not be caught by XSD validation alone.
58
+ ```
59
+
60
+ That's a real example, not a mockup — every command in this README was actually run before being written down. No payment file handy? Generate one:
61
+
62
+ ```bash
63
+ tollgate generate --count 1 --rule-id charset_violation --output-dir /tmp/fixtures
64
+ tollgate validate /tmp/fixtures/charset_violation_0.xml
65
+ ```
66
+
67
+ → **[Full usage guide](docs/usage.md)** — every command, every flag, the Python library API, batch directory checking, the GitHub Action
68
+ → **[Why this exists](docs/why.md)** — the dated deadline behind it, the AI design philosophy, what was found and fixed during development
69
+ → **[Sources](docs/SOURCES.md)** — every rule traced to a citation
70
+
71
+ ## What it checks
72
+
73
+ One message type in v1: **pacs.008.001.08**, the FI-to-FI customer credit transfer used across Fedwire, CHIPS, and SWIFT CBPR+.
74
+
75
+ | Check | Catches |
76
+ |---|---|
77
+ | Schema validity | Standard XSD structural validation. The floor everything else stands on. |
78
+ | SWIFT character set | A character outside SWIFT's allowed set — schema-valid, network-invalid. |
79
+ | Address structure | Free-format addresses used where structure is required, or line counts the schema allows but a network's guidelines don't. |
80
+ | Truncation signals | A value landing at exactly 35 or 70 characters — old legacy line limits — in a field with a much higher modern limit. Reported as a warning, not a certainty. |
81
+ | Network-mandatory gaps | Fields the schema marks optional that a real network requires in practice (e.g. UETR for Fedwire). |
82
+
83
+ Every rule traces to a primary source — see [`docs/SOURCES.md`](docs/SOURCES.md). No rule ships without one.
84
+
85
+ ## Not a compliance tool
86
+
87
+ Tollgate is a developer-facing sanity check, not a replacement for SWIFT certification or MyStandards testing. It covers one message type, checks structure and format (not business logic like BIC reachability), and is explicit in [`docs/why.md`](docs/why.md) about every limitation found during development. If it can't catch something, the docs say so.
88
+
89
+ ## Three ways to use it
90
+
91
+ | | |
92
+ |---|---|
93
+ | **CLI** | `tollgate validate payment.xml` · `tollgate validate-dir payments/` |
94
+ | **Python library** | `from tollgate import check_message, check_file, check_directory` |
95
+ | **CI** | `uses: iso-tollgate/tollgate/.github/actions/validate@main` |
96
+
97
+ Details and examples for all three: [`docs/usage.md`](docs/usage.md).
98
+
99
+ ## Status
100
+
101
+ 166 tests passing (163 deterministic/local + 3 live API tests, all confirmed passing against the real Anthropic API), 0 skipped when an `ANTHROPIC_API_KEY` is set — and 163 passing with the 3 API tests skipping cleanly when it isn't, so the full deterministic suite (every validation rule, the generator, the eval harness, both APIs) needs zero API key to verify.
102
+
103
+ `--explain` has been live-tested against the real model: it correctly names the violated field and cause, and correctly hedges on warning-severity (heuristic) findings rather than asserting them as certain failures — verified, not assumed.
104
+
105
+ Not yet on PyPI or Homebrew — clone-and-install is the path for now. A Homebrew tap is scaffolded in [`homebrew-tollgate/`](https://github.com/iso-tollgate/homebrew-tollgate) for once a tagged release exists.
106
+
107
+ ## License
108
+
109
+ Apache 2.0. See [`LICENSE`](LICENSE).