Metadata-Version: 2.4
Name: verifyhash-einvoice
Version: 0.2.7
Summary: Zero-dependency EN 16931 / XRechnung (UBL + CII) e-invoice conformance validator — 297 business rules including the national BR-DE and BR-DEX CIUS/extension layers, each differential-tested to 0 divergences against the official Schematron within the implemented set; every fireable BR-CL-* code-list check is now implemented in both syntaxes; see CORRECTNESS.md for the honest remaining scope.
Project-URL: Homepage, https://github.com/verifyhash/verifyhash
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Accounting
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: NOTICE
Dynamic: license-file

# einvoice

A zero-dependency, embeddable, self-hostable conformance validator for
**EN 16931** electronic invoices, targeting the German **XRechnung** CIUS
(UBL 2.1 `Invoice` and UN/CEFACT CII syntaxes, including Factur-X/ZUGFeRD
PDF/A-3 containers — `einvoice validate invoice.pdf` extracts and grades the
embedded XML directly, with the standard library only).

- **Zero dependency.** Python 3 (>=3.8) standard library only. No lxml, no
  Java, no Schematron toolchain, no network calls. `pip install
  verifyhash-einvoice` gets you the released package and its `einvoice`
  console script from PyPI; from a checkout, `python3 einvoice.py validate
  x.xml` needs no install at all and `pip install .` installs that exact tree
  (`pyproject.toml` pins `dependencies = []` — a tested contract, see
  `test_packaging.py`).
- **Embeddable.** The validator is a small pure-Python package
  (`einvoice/parser.py`, `einvoice/rules.py`, `einvoice/validate.py`,
  `einvoice/cli.py`); rules are plain functions over a parsed model, so an
  ERP or billing system can import it in-process instead of shelling out to
  a validator service — copying the bare `einvoice/` package directory into
  your tree is a supported (and tested) install method.
- **Self-hostable.** Runs fully offline — zero network egress, enforced at
  the socket layer by `test_network_egress.py` (see
  [`SECURITY.md`](SECURITY.md)). The rule corpus and test
  fixtures are vendored in-repo (`corpus/`), so the thing you validate against
  is auditable and pinned — no dependency on a third-party validation API. The
  supply-chain posture (zero runtime deps, offline, vendored corpus) is
  written up for evaluators in [`SECURITY.md`](SECURITY.md), backed by a
  committed CycloneDX 1.5 SBOM at [`sbom/bom.json`](sbom/bom.json).
- **CI-gateable.** `ci/` ships a copy-paste build gate (POSIX sh + GitHub
  Actions / GitLab CI recipes) that fails a build on any non-conformant
  invoice, naming the violated rule ID. See [§4](#4-ci-conformance-gate).
- **Deutsche Anleitung.** A German-language quickstart whose shell commands
  are kept byte-identical to the English docs by an automated parity check
  (`test_install_command_drift.py`): [`QUICKSTART.de.md`](QUICKSTART.de.md).

### Safe on untrusted input

Supplier XML is untrusted input, so every production parse entry point is
hardened against the classic `xml.etree`/expat attack classes — using **only
the standard library** (`xml.etree.ElementTree` + `xml.parsers.expat`, no
`lxml`, no `defusedxml`), so the zero-runtime-dependency contract above is
unchanged and still proven by `test_packaging.py`. Concretely, the parser:

- **rejects any `<!DOCTYPE>`/DTD** — internal *or* external subset — at the
  expat `StartDoctypeDeclHandler`, *before* any entity can be defined;
- therefore **defines and expands no custom entity** (a billion-laughs or
  quadratic-blowup payload aborts in constant time and memory, never
  materialising the expanded string), and **resolves no external entity or
  external DTD** — expat opens no `file://`/`http://` URL, so an XXE such as
  `<!ENTITY xxe SYSTEM 'file:///etc/passwd'>` reads nothing (no file-read, no
  SSRF);
- turns a refused payload into a **bounded, actionable failure** — the engine's
  ordinary *not-well-formed* report finding and CLI exit code `3`, identical to
  any ill-formed invoice — **never a crash, a hang, or a silent pass**.

Full guarantee (including the byte-length / element-count / nesting-depth
resource ceilings on well-formed-but-hostile input) is written up in
[`SECURITY.md`](SECURITY.md) under **"Untrusted input / XML entity handling"**,
and is proven end-to-end by `test_security.py` (billion-laughs,
quadratic-blowup, `file://` external-entity read with a secret-canary
leak check, `/etc/passwd` XXE, external-DTD `SYSTEM`) and `test_robustness.py`,
which also assert that a benign XRechnung invoice still parses and validates
unchanged.

Read §2 before trusting it with anything. The engine asserts **297 business
rules** in total — the exact set the code fires, enumerated per rule in
[`COVERAGE.md`](COVERAGE.md) / `coverage_matrix.json` and drift-gated by
`test_coverage_matrix.py` against the live rule registries. That total breaks
down as: **219 of the 223 official EN 16931 `BR-*` rule ids** in each CEN
syntax universe (UBL and CII) — **every official rule that can actually fire**
(every fireable `BR-CL-*` code-list check is now implemented in both
syntaxes) — plus, with
`--profile=xrechnung`, the German XRechnung CIUS + extension layer
(all 55 `BR-DE-*`/`BR-DE-CVD-*`/`BR-TMP-*`/`BR-DEX-*` asserts the UBL
artifact carries, and all 49 the CII artifact carries — including the
CII-only `BR-TMP-3` and `BR-DEX-15`) and the **21
`PEPPOL-EN16931-R*` rules KoSIT ships inside the official XRechnung
Schematron artifact** — the KoSIT-vendored subset only, **not** Peppol BIS
Billing 3.0 support (see §2). The machine-checked gap of official rules
neither implemented nor documented as a deliberate exclusion is **zero for
both universes** (`fireable_missing == 0`): `test_coverage_gap.py` recomputes
it from the vendored official Schematron on every run and fails if the
fireable-missing count is ever nonzero. The last admitted gap in the KoSIT
XRechnung artifact — the Clean-Vehicle-Directive / temporary family
(`BR-DE-CVD-*`, `BR-TMP-*`) — is **closed with differential proof in both
bindings** as of 2026-07-11; the machine-checked KoSIT worklists
(`cvd_tmp_family` and `peppol_kosit_family` in `coverage_matrix.json`) are
empty.

The caveat that keeps that claim honest, stated adjacent to it rather than in
a footnote: **4 official ids (`BR-CO-05`–`BR-CO-08`) are shipped as literal
`test="true()"` tautologies** in the CEN artifacts — asserts that can never
fire, in either universe, so implementing them with a differential proof is
impossible *by construction*. Those 4 tautologies are documented with
verbatim artifact evidence in
[`COVERAGE.md`](COVERAGE.md), the generated per-rule matrix that is the
authoritative inventory (it supersedes the partial family-orientation tables in
§2, which name only about half the core rules, wherever they disagree). A related asymmetry worth knowing: the total-VAT
decimal pair `BR-DEC-13`/`BR-DEC-15` is implemented and
differential-proven on the **CII** leg (that artifact ships real numeric
round2 tests), while the **UBL** artifact's asserts for the same pair are
vacuous-by-defect (a predicate-context bug means they can never fire there)
— the engine asserts the stated ≤2-decimals intent on UBL anyway, as
documented deliberate strictness. This is deliberately **not** an
uncaveated "100%" claim.

**How correctness is proven:** every implemented rule is differential-tested
against the **official, normative compiled Schematron artifacts** (the legal
rulesets). The 2026-07-11 run, all four legs at **zero divergences**:
EN 16931 core on UBL — 209 rules × 1145 real invoices = **239,305
comparisons**; XRechnung + KoSIT-vendored Peppol on UBL — 76 graded ids ×
1067 invoices = **81,092 comparisons**; EN 16931 core on CII — the 96-rule
graded subset × 111 invoices = **10,656 comparisons**; XRechnung + Peppol on
CII — 52 graded ids × 117 invoices = **6,084 comparisons**. See
[`CORRECTNESS.md`](CORRECTNESS.md) for the method, corpora, and the honest
limits of those claims.

**CII proof parity — the worklist is now CLOSED.** The two bindings share one
rule registry, but a rule only earns `syntax = UBL + CII` in the matrix once
it is differentially proven on the CII artifact too, and which rules have that
proof is machine-tracked: `test_cii_parity.py` recomputes the worklist
(`cii_parity.json`) from the live coverage matrix plus a real XML parse of the
vendored CII Schematron, and fails on any drift, so the parity gap can neither
be hand-edited nor go stale. As of 2026-07-23 the arc is terminal: **281 of
the 297 asserted rules are differential-proven on both the UBL and CII
bindings, 12 are officially UBL-only, and 4 are CII-only** (`BR-TMP-3` and
`BR-DEX-15`, whose asserts exist only in the CII artifact, plus
`BR-DEC-13`/`BR-DEC-15`, whose UBL asserts are artifact-vacuous — see the
caveat in §1). The German KoSIT layer is complete on BOTH bindings: all
**55** UBL and all **49** CII German-family asserts
(`BR-DE-*`/`BR-DE-CVD-*`/`BR-TMP-*`/`BR-DEX-*`) the vendored artifacts carry
are implemented and differentially graded — including the extension-profile
scheme-id and attachment checks `BR-DEX-01/04/05/06/07/08` on CII.
**Zero rules remain on the cii-fireable worklist** — every one of the 12
UBL-only rules the vendored CII artifacts were measured against is resolved
with evidence: **4 are cii-artifact-defective** (the CII artifact ships them
as a `test="true()"` tautology or bound to a row whose `every $rate in ()` is
vacuously true — they can never fire) and **8 are binding-inapplicable**
(`BR-DEX-02/03/09/10/11/12/13/14`: carried by no vendored CII artifact at all, so a
CII proof is impossible by construction). Every one of those reasons is
re-verified live against the vendored artifacts on each run; the committed
worklist and `COVERAGE.md` are the authoritative state.

---

## 1. Why this exists: the legal forcing function

Structured e-invoicing is stopping being optional in the EU. If you issue or
receive B2B invoices there, a conformance validator moves from "nice tooling"
to "the thing that decides whether your invoice legally exists."

- **Germany (2025–2028).** Since 1 January 2025 every German business must be
  able to **receive** EN 16931-conformant e-invoices (XRechnung or ZUGFeRD);
  the obligation to **issue** them phases in through 2027–2028 depending on
  turnover, as legislated in the Wachstumschancengesetz. Public-sector
  suppliers have been required to send XRechnung since 2020.
- **France (2026–2027).** Reception of structured e-invoices becomes
  mandatory for all VAT-registered businesses in **September 2026**; the
  obligation to issue phases in September 2026 (large/mid-size) through
  **September 2027** (SMEs), via the PDP/e-reporting reform. The French
  formats (Factur-X, UBL, CII) are all EN 16931 profiles.
- **EU ViDA.** The "VAT in the Digital Age" package (adopted 2025) makes
  structured e-invoicing the default for intra-EU B2B and adds digital
  reporting requirements on a ~2030 horizon — again on the EN 16931 core.

Dates above reflect the legislation as understood at time of writing
(mid-2026); phase-ins shift, so verify against current law before relying on
them. The direction, however, is one-way: every invoice will need to pass
machine-checkable conformance rules, and the party that fails them eats the
rejection, the payment delay, or the VAT problem. Buyers on a deadline need a
validator they can run themselves, embed in their pipeline, and audit — not a
black-box web form.

---

## 2. HONEST coverage — read this before using it

**Profile:** XRechnung 3.x (the German CIUS of EN 16931-1:2017), UBL 2.1
`Invoice` and UN/CEFACT CII syntaxes.

**Live scope, stated before anything else.** The engine asserts **297 business
rules**. That is **219 of the 223 official EN 16931 `BR-*` assert ids** in each
CEN syntax universe — the four it leaves are `BR-CO-05`, `BR-CO-06`, `BR-CO-07`
and `BR-CO-08`, which CEN ships as literal `test="true()"` tautologies that can
never fire for anyone — plus **55** German national asserts on UBL and **49** on
CII (55 of 55 and 49 of 49 of what the official KoSIT XRechnung 3.0.2
Schematrons carry: the `BR-DE-*` CIUS core, the `BR-DEX-*` extension profile and
the `BR-DE-CVD-*`/`BR-TMP-*` families), plus the **21 KoSIT-vendored
`PEPPOL-EN16931-R*`** asserts. Verify it in one command without reading further:
`python3 -m einvoice info --json` prints `"rule_count": 297`, and
`python3 -c "from einvoice import rules, rules_xrechnung; print(len(rules.ALL_RULES), len(rules_xrechnung.ALL_RULES), len(rules_xrechnung.CII_DE_RULES))"`
prints `219 55 49`.

> **The two tables below are NOT the implemented set — do not size the engine
> from them.** They are a partial, family-by-family orientation view: the core
> table names 108 concrete ids, about half of the 219 the engine asserts, and it
> is kept only because reading "is this family covered at all?" off a short
> table is faster than scanning 297 rows.
> [`COVERAGE.md`](COVERAGE.md) / `coverage_matrix.json` — regenerated from the
> live rule registries by `gen_coverage.py` and drift-gated against them by
> `test_coverage_matrix.py` — is the **authoritative per-rule inventory**, with
> the official rule text and per-syntax proof state for every one of the 297
> ids. Where it and these tables disagree, it wins.

### Implemented — EN 16931 core (family orientation; full inventory in COVERAGE.md)

| Family | Rule IDs |
|---|---|
| Header existence/cardinality | BR-01, BR-02, BR-03, BR-04, BR-05, BR-06, BR-07, BR-08 |
| Seller/Buyer postal address | BR-09 (seller country code), BR-10 (buyer postal address), BR-11 (buyer country code) |
| Payee & Seller tax representative | BR-17 (payee name when payee differs from seller), BR-18 (tax representative name), BR-19 (tax rep postal address), BR-20 (tax rep country code) |
| Payment instructions | BR-49 (payment means type code), BR-50/BR-61 (credit-transfer account id), BR-51 (card PAN truncation — official `warning` flag, non-blocking) |
| References & addresses | BR-55 (preceding invoice reference), BR-57 (deliver-to country code), BR-62/BR-63 (seller/buyer electronic-address scheme id) |
| Document totals presence | BR-12 (Σ line net), BR-13 (total w/o VAT), BR-14 (total with VAT), BR-15 (amount due) |
| Invoice-line cardinality | BR-16, BR-21, BR-22, BR-24, BR-26 |
| Invoice-line content | BR-25 (item name), BR-27 (net price not negative), BR-28 (gross price not negative), BR-29/BR-30 (invoicing / line period end >= start), BR-CO-04 (line VAT category code) |
| Allowance/charge existence | BR-31, BR-32, BR-33, BR-36, BR-37, BR-38, BR-41, BR-42, BR-43, BR-44 |
| Code list | BR-CL-01 (UNTDID 1001 invoice type code) |
| Arithmetic co-constraints | BR-CO-10, BR-CO-13, BR-CO-14, BR-CO-15, BR-CO-16, BR-CO-17 |
| VAT breakdown presence | BR-CO-18 |
| VAT breakdown group (BG-23) | BR-45 (taxable amount), BR-46 (tax amount), BR-47 (category code), BR-48 (category rate) |
| VAT-category consistency | BR-S-01, BR-Z-01, BR-AE-01, BR-E-01, BR-G-01, BR-IC-01, BR-O-01 |
| Standard-rated (S) category | BR-S-02/03/04 (Seller VAT id for S line/allowance/charge), BR-S-05/06/07 (S rate > 0), BR-S-09 (tax = taxable × rate), BR-S-10 (no exemption reason on S) |
| Zero-rated (Z) category | BR-Z-02/03/04 (Seller VAT id for Z line/allowance/charge), BR-Z-05/06/07 (Z rate = 0), BR-Z-08 (taxable = Σ Z line nets − allowances + charges), BR-Z-09 (tax = 0), BR-Z-10 (no exemption reason on Z) |
| Exempt (E) category | BR-E-02/03/04 (Seller VAT id for E line/allowance/charge), BR-E-05/06/07 (E rate = 0), BR-E-08 (taxable = Σ E line nets − allowances + charges), BR-E-09 (tax = 0), BR-E-10 (exemption reason text/code REQUIRED on E) |
| Decimal precision (max 2 places) | BR-DEC-01, BR-DEC-02, BR-DEC-05, BR-DEC-06, BR-DEC-09, BR-DEC-10, BR-DEC-11, BR-DEC-12, BR-DEC-14, BR-DEC-16, BR-DEC-17, BR-DEC-18, BR-DEC-19, BR-DEC-20, BR-DEC-23 |

Plus two structural checks: S-WF (well-formed XML) and S-ROOT (the root is a
gradeable one — UBL `Invoice`, UBL `CreditNote`, or CII `CrossIndustryInvoice`).
Rule wording follows the vendored EN 16931 Schematron
(`corpus/cen-en16931/ubl/schematron/abstract/EN16931-model.sch`) verbatim.

### Implemented — XRechnung CIUS layer (`--profile=xrechnung`; family orientation, full inventory in COVERAGE.md)

| Family | Rule IDs |
|---|---|
| Mandatory German fields | BR-DE-1 (payment instructions), BR-DE-15 (BuyerReference), BR-DE-2/5/6/7 (seller contact + name/phone/email), BR-DE-3/4 (seller city/post code), BR-DE-8/9 (buyer city/post code), BR-DE-10/11 (deliver-to city/post code), BR-DE-14 (VAT rate per breakdown) |
| Seller VAT identification | BR-DE-16 |
| Type-code / spec-id restrictions | BR-DE-17, BR-DE-21, BR-DE-26 |
| Payment-means grouping | BR-DE-23-a/-b (credit transfer), BR-DE-24-a/-b (card), BR-DE-25-a/-b (direct debit), BR-DE-30, BR-DE-31 (SEPA mandate fields) |
| Content quality (warnings) | BR-DE-19, BR-DE-20 (IBAN mod-97), BR-DE-27 (phone), BR-DE-28 (email), BR-DE-18 (Skonto grammar, fatal) |
| Delivery-date recommendation | BR-DE-TMP-32 (information) |

That is every `BR-DE-*` assert in the official KoSIT XRechnung 3.0.2 UBL
Schematron (the numbering has official gaps: no BR-DE-12/13/29 exist there).
Those 32 CIUS-core asserts are only part of the German layer, though: counting
the extension and CVD/TMP families described in the next paragraph, the engine
asserts **55 German ids on UBL and 49 on CII** — every assert either official
artifact carries. Severities mirror the official flags — only **fatal** rules affect the exit
code; warnings/information are reported in `--json`. The 15 `BR-DEX-*`
extension-profile rules have since been implemented as well (14 UBL asserts
plus the CII-only `BR-DEX-15` — sub invoice lines unsupported, warning; the
seven extension asserts the CII artifact carries — `BR-DEX-15` plus the
scheme-id/attachment group `BR-DEX-01/04/05/06/07/08` — are all graded on the
`xrechnung-cii` differential leg), and so has the
complete CVD/TMP family the same artifacts carry: the Clean-Vehicle-Directive
profile (`BR-DE-CVD-01`–`05`, `BR-DE-CVD-06-a/-b`, `BR-TMP-CVD-01` — gated on
the CVD `CustomizationID` `…xrechnung:cvd_0.9`, inert on plain invoices),
`BR-TMP-2` (BT-124 must be an absolute URL; warning), and `BR-TMP-3` — which
exists **only in the CII binding** (the vendored UBL artifact carries no such
assert), so it is implemented and proven on CII only (`syntax = CII` in
`COVERAGE.md`). Every family rule is differential-proven in each binding
whose artifact carries it, with dedicated FIRE and PASS fixtures; the family
enumeration is machine-checked (`cvd_tmp_family` in `coverage_matrix.json`,
recomputed live by `test_coverage_gap.py`). All 21 `PEPPOL-EN16931-R*` rules
the same KoSIT artifact vendors are implemented in both bindings
(differential-proven; the family enumeration stays machine-checked in
`COVERAGE.md`) — that is the KoSIT-vendored subset only, **not** Peppol BIS
Billing 3.0 support.

### Differential result vs. the OFFICIAL Schematron (this run)

The strongest correctness evidence: `differential.py` runs each invoice through
the **official, normative** compiled Schematron artifacts (Saxon → SVRL) and
through our validator, then compares — for every invoice and every graded rule
ID — whether each engine fires. The Schematron is the legal artifact; any
disagreement is our bug. The 2026-07-11 run:

```
LEG 1  EN 16931 core, UBL ............... 1145 invoices x 209 rules = 239,305
LEG 2  XRechnung + Peppol batch, UBL .... 1067 invoices x  76 rules =  81,092
LEG 3  EN 16931 core, CII ............... 111 invoices x 96 graded  =  10,656
LEG 4  XRechnung + Peppol batch, CII .... 117 invoices x 52 graded  =   6,084
TOTAL AGREEMENT ...... 100.0000% on every leg
divergences .......... 0 false-positives + 0 misses, all legs
```

All implemented rules agree with their official Schematron on every graded
invoice. Reproduce it (needs `saxonche` importable): `python3 differential.py`
(or `... en` / `... xrechnung` for one leg). Method, corpus breakdown, the
divergences that were found and fixed, and the honest scope limits are
documented in [`CORRECTNESS.md`](CORRECTNESS.md). This proves faithfulness
**only for the graded rule × syntax pairs** — the per-rule graded sets, and
the reasons some implemented rules are not graded on CII, are enumerated in
[`COVERAGE.md`](COVERAGE.md) — not EN 16931 or XRechnung as a whole (see §2
"NOT covered").

### Conformance result (this run)

`conformance.py` drives the real CLI as a subprocess over every vector in
`corpus/vendored/` (14 valid + 52 invalid). The invalid vectors are Difi
`<testSet>` files, so the harness extracts every embedded `<Invoice>`
fragment and checks each `<error>`/`<success>` assertion individually —
284 embedded assertions in total.

```
total vendored vectors ............. 66  (14 valid + 52 invalid)

VALID-vector pass rate ............. 14/14   100.0%   (miss = FALSE POSITIVE)
COVERED-INVALID detection rate ..... 52/52   100.0%   (correct rule id fired)
OUT-OF-SCOPE invalid vectors ....... 0

embedded-block detail (Difi assertions):
  <error>   fragments: 140 total -> 140 detected, 0 missed, 0 wrong-id, 0 oos
  <success> fragments: 144 total -> 144 clean,  0 FALSE POSITIVE,   0 oos

HARD FAILS: 0   -> RESULT: PASS
```

Every covered invalid vector is detected with the **correct labeled rule ID**
across all 140 error fragments; every valid vector and all 144 must-pass
fragments come back clean — zero false positives on this corpus.

The harness itself was mutation-tested (then the code restored
byte-identical): neutering `br_06` produced 4 `WRONG RULE ID` hard fails;
forcing `br_01` to always fire produced 13 `FALSE POSITIVE` hard fails;
removing `BR-Z-01` was correctly reported as out-of-scope, not silently
passed. A green run means something because the harness demonstrably goes red.

Reproduce it: `cd einvoice && python3 conformance.py` (exit 0 = pass; exit 1
prints the offending file, block, and expected vs. actual rule IDs).

### KoSIT test-suite conformance headline

End-to-end, on KoSIT's *own* official documents: **39 of 39** in-scope
XRechnung test-suite documents in **UBL** syntax (the plain `xrechnung_3.0`
CIUS) are classified exactly as the suite labels them — accepted as valid. The
same holds for the **CII (UN/CEFACT)** binding: **39 of 39** in-scope
`*_uncefact.xml` documents, routed through the shipped CII engine
(`parser_cii` + `rules.ALL_RULES` + `rules_xrechnung.evaluate_cii`), are
classified exactly as the suite labels them. The 8 out-of-scope documents (the
extension and CVD guidelines, in both syntaxes) are machine-listed with their
exact reason in
[`testsuite_conformance.json`](testsuite_conformance.json); see
[`CORRECTNESS.md` §4a](CORRECTNESS.md) for provenance and scope.

### NOT covered yet (deliberate scope limits — do not rely on these)

- **`BR-TMP-3` and `BR-DEX-15` are CII-only by artifact design** — the
  `BR-DE-*` CIUS core, the `BR-DEX-*` extension layer AND the
  Clean-Vehicle-Directive/temporary family (`BR-DE-CVD-*`, `BR-TMP-CVD-01`,
  `BR-TMP-2`, `BR-TMP-3`) are all implemented (see `COVERAGE.md`), but the
  vendored UBL artifact carries no `BR-TMP-3` or `BR-DEX-15` assert, so those
  two rules are asserted and proven on the CII syntax only — a UBL
  differential proof is impossible by construction.
- **No Peppol BIS Billing 3.0 support.** All 21 `PEPPOL-EN16931-R*`
  rules KoSIT ships inside the official XRechnung Schematron artifact ARE
  implemented (both bindings, differential-proven; the enumeration stays
  machine-checked in `COVERAGE.md`), but the OpenPeppol ruleset
  proper (its own Schematron + test corpus) is a separate, not-vendored
  artifact — nothing beyond the KoSIT-vendored asserts is claimed.
- **French national CIUS not yet checked (the EN 16931 core for French
  invoices IS).** This is a technical scope statement distinct from the French
  *legislative* timeline in [§1](#1-why-this-exists-the-legal-forcing-function)
  (reception mandatory Sept 2026, issuance phasing to Sept 2027). A French B2B
  invoice — Factur-X (a CII payload in a PDF/A-3 container), UBL, or CII — is an
  EN 16931 profile, so the full EN 16931 core (`BR-*` semantic model) already
  applies to it here, and the **Factur-X** path is first-class: the shipped PDF
  container extractor (`einvoice.pdf_container`) pulls the embedded
  `CrossIndustryInvoice` XML and validates it to EN 16931 depth via the exact
  same core engine (`einvoice.rules.ALL_RULES`) — measured end-to-end by
  `test_facturx_profile_scope.py`, and detailed in `COVERAGE.md` under
  "French CIUS scope". What a **French CIUS** adds on top of EN 16931 — the
  **FNFE-MPE / Chorus Pro** Factur-X French national business rules and code
  lists — is **not** checked: that official Schematron is **not vendored** in
  `corpus/`, so those French-CIUS-specific rules are out of scope until it is
  (nothing French-specific is fabricated or approximated from prose). So
  "validates the EN 16931 core for a French invoice" is TRUE and useful, while
  **full French CIUS conformance is NOT claimed** — the same honesty label this
  README applies to Peppol BIS Billing 3.0 above.
- **4 official `BR-*` ids per CEN universe are documented deliberate
  exclusions, NOT coverage**: the 4 official **tautologies**
  `BR-CO-05`–`BR-CO-08`, shipped as literal `test="true()"` asserts in both
  CEN universes, so no implementation could ever be differentially proven
  for them. The `BR-CL-*` code-list class carries no deferrals — the engine
  asserts every fireable code-list check in both syntaxes. Per-rule
  reasons and verbatim artifact evidence:
  [`COVERAGE.md`](COVERAGE.md) §Exclusions.
- **No XSD (structural schema) validation.** Layer S-XSD is deferred; only
  well-formedness and the root element are checked structurally.
- **UBL `CreditNote` IS validated; no signatures.** A UBL 2.1 `CreditNote`
  (root `CreditNote-2:CreditNote`) is routed through the SAME EN 16931 core
  engine as an `Invoice` (the official Schematron binds both roots
  symmetrically), differentially proven at 0 divergences over the vendored
  CreditNote corpus — see `COVERAGE.md` §"UBL CreditNote scope". CII
  (`CrossIndustryInvoice`) and Factur-X/ZUGFeRD PDF containers ARE also
  validated — since 0.2.7 by `einvoice validate <invoice.pdf>` itself as well
  as by `einvoice.report` / `validate-batch` — with the graded CII rule subsets
  in `COVERAGE.md`. The container reader handles classic-layout PDFs; encrypted,
  cross-reference-stream (PDF 1.5+) and truncated files are refused as
  `unsupported-container` (exit 3), never guessed at. XML signatures are not
  checked.
- **The 100% figures are agreement/pass rates for the implemented, graded
  rule × syntax pairs only** (the `differential.py` legs and the
  `conformance.py` vendored vectors quoted above). They are 100% of a
  limited, honest scope, **not** 100% of the standard (structural XSD
  validation is not performed, and the national CIUS layers beyond German
  XRechnung are out of scope).

See `SPEC.md` §6 for the full deferred list.

---

## 3. Install / embed / usage

> **New here?** [`QUICKSTART.md`](QUICKSTART.md) is a tested 5-minute copy-paste
> path: install, validate a passing and a failing sample invoice, and read the
> exit code + `--json` — every command in it is run against the live engine by
> `test_quickstart.py`.

Three ways in, one code path (`einvoice/cli.py` — proven identical by
`test_packaging.py`). Mind the labels in the block below: the `(a)` and `(b2)`
forms name `einvoice.py` or a local source directory, so they need a
**repository checkout** (or a vendored copy of one); the `(b)` forms are what
the published wheel gives you and need nothing from this repo.
`test_doc_commands_from_wheel.py` enforces that split.

```sh
# a) straight from a checkout — nothing to install
python3 einvoice.py validate <invoice.xml> [--json] [--profile=en16931|xrechnung]
python3 -m einvoice   validate <invoice.xml> [--json] [--profile=en16931|xrechnung]

# validate a whole folder or glob in one run (reuses the same rule engine):
python3 -m einvoice   validate-batch <dir|glob> [--json] [--quiet] [--profile=…]

# b) pip-install the released package from PyPI — the normal route
python3 -m pip install verifyhash-einvoice
einvoice validate <invoice.xml> [--json] [--profile=en16931|xrechnung]
einvoice validate-batch <dir|glob> [--json] [--quiet] [--profile=en16931|xrechnung]

# b2) …or install a checkout / vendored copy instead — same code, no package
#     index needed: the right answer for air-gapped CI or an exact pin
python3 -m pip install /path/to/einvoice     # zero runtime dependencies
```

**c) embed in-process** — vendor the bare `einvoice/` package directory (the
pure-Python package alone, no corpus needed at runtime) or pip-install it,
then:

```python
from einvoice import validate_file, NotWellFormed

result = validate_file("invoice.xml", profile="xrechnung")
if not result.ok:
    for v in result.violations:          # each: rule_id, message, element
        print(v.rule_id, v.message)      # e.g. "BR-DE-15 The element ..."
```

`--profile=xrechnung` layers the 55 German `BR-DE-*`/`BR-DE-CVD-*`/
`BR-TMP-*`/`BR-DEX-*` rules on top of the core (default profile: core only).

Exit codes (stable contract):

| Code | Meaning |
|---|---|
| 0 | passes every implemented **fatal** rule (warnings may still be reported) |
| 1 | at least one implemented fatal rule failed |
| 2 | usage error (bad args, missing file, unknown profile) |
| 3 | input is not well-formed XML |

Default output on failure is the **first** fatal violated rule, human message,
and offending element, followed by two indented lookup lines — `how to fix:
einvoice --explain <RULE-ID>` and, when the shipped remediation catalogue
covers that rule, its `rule page:` URL under
`https://verifyhash.com/einvoice/rules/`. On a **pass** with non-fatal
findings, the same summary lists them (`[<severity>] <RULE-ID>: <message>`,
first 10, then a count and a pointer to `--format json`); the verdict line
still reads `PASS:` and, at the default `--fail-on fatal`, the exit code is
still `0`. `--json` emits the full
machine-readable result:

```json
{
  "source": "invoice.xml",
  "valid": false,
  "violation_count": 2,
  "violations": [
    {
      "rule": "BR-06",
      "message": "An Invoice shall contain the Seller name (BT-27).",
      "element": "cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName",
      "severity": "fatal",
      "field": "cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName",
      "title": "An Invoice shall contain the Seller name (BT-27).",
      "fix_hint": "Add the required element at `cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName`: An Invoice shall contain the Seller name (BT-27).",
      "terms": ["BT-27"],
      "location": "cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName"
    }
  ]
}
```

The first four keys are the identity of the finding; `field` repeats `element`
under the name `python3 -m einvoice.report` uses, and `title` / `fix_hint` /
`terms` / `location` are relayed from the committed
[`remediation_catalog.json`](remediation_catalog.json) so a CI job can print
*what to change*, not just a rule id. All five are always present: a rule with
no catalog entry emits `null` / `[]` rather than dropping the keys. A violation
that could be attributed to a concrete source position additionally carries
`source_line` (that one key really is conditional).

A `valid: true` result means "no implemented fatal rule fired" — given §2, it
does **not** yet mean "legally conformant XRechnung."

### CLI contract

The command line is a small, stable surface. Everything below is pinned by
`test_cli.py` (ergonomics) and `test_cli_sb.py` / `test_packaging.py` (the
exit-code and packaging invariants).

**Exit codes** — the whole point of the tool is to gate a build, so the exit
code is the contract, not the prose. The `receipt` subcommand collapses code
`3` into a FAIL receipt (exit `1`) because a receipt must always emit a
document; codes `2` and `3` below therefore describe `validate`. A receipt's
tamper-evidence is a recompute-and-compare of `content_sha256` over the
canonical body. `einvoice receipt --verify <receipt.json>` runs exactly that
check for you in one command (details under **`receipt --verify`** below); the
same check is reproducible in any language without our binary — see
[`RECEIPT-VERIFICATION.md`](RECEIPT-VERIFICATION.md) for the exact recipe and its
honest limit (the outer hash is a body digest, not self-covering).

| Code | Meaning |
|---|---|
| `0` | passes every implemented **fatal** rule (advisory warnings may still be reported) |
| `1` | at least one implemented **fatal** rule failed |
| `2` | usage error — bad/missing arguments, no such file, or an unknown `--profile` |
| `3` | input is **not well-formed** XML (or a rejected DTD/entity/XXE payload, folded to the same not-well-formed outcome) — `validate` only |

Syntax-binding warnings and XRechnung `warning`/`information` findings are
**advisory**: they are reported but never move the exit code off `0`/`1`.

**Flags**

- `--version` — prints the packaged `einvoice.__version__` (read from the
  installed package, not a hardcoded string) and exits `0`. It takes precedence
  over everything: no subcommand or file is required
  (`einvoice --version` / `python3 -m einvoice --version`).
- `--help` / `-h` — print the usage synopsis plus a one-line description of every
  command (`validate`, `validate-batch`, `receipt`, `info`, `--show-config`,
  `--version`) to **stdout** and exit `0`. Like `--version` it is an
  informational precedence flag — handled before any subcommand or file, so
  `einvoice --help` is a valid first command; `-h` is a byte-identical alias.
  It changes no validation behaviour and reads no input file. The two-word set of
  commands it lists is bound to the `VALID_SUBCOMMANDS` registry by
  `test_cli_help.py`, so a new subcommand cannot go silently undocumented.
- `--json` — emit the full machine-readable result instead of the human
  summary (shape below and in [`REPORT-SCHEMA.md`](REPORT-SCHEMA.md)).
- `--format <fmt>` (also `--format=<fmt>`) — **select the output shape** on
  `validate` and `validate-batch`. The vocabulary is the same nine names
  `einvoice info` advertises under `formats`, read from the one registry
  (`einvoice.report.REPORT_FORMATS`) rather than a second list: `json`, `junit`,
  `sarif`, `gitlab`, `github`, `azure`, `html`, `badge`, `text`. `--format json`
  is an **exact alias** for `--json` (same code path, byte-identical output) and
  `--format text` is the default human summary, so the flag is purely additive.
  The other seven are rendered by `einvoice.report.render_report` — the single
  emitter dispatch `python3 -m einvoice.report --format <fmt>` itself uses — so
  the report bodies are identical on both surfaces for the same invoice and
  profile. The common CI line is
  `einvoice validate --format sarif invoice.xml > results.sarif`, which GitHub
  code scanning ingests directly. The verdict is graded by **this** subcommand's
  rules: its `en16931` default profile (the sibling entry point defaults to
  `xrechnung` — pass `--profile` explicitly when comparing them), its
  `--fail-on` threshold, and the same non-blocking treatment of syntax-binding
  findings the text/JSON forms use. `validate-batch` accepts the
  aggregate-capable subset `json`/`junit`/`text`; the other six describe one
  invoice, so a directory under those is a usage error (`2`) naming the per-file
  command. An unknown format, `--format` with no value, `--format` twice with
  conflicting values, or `--format` together with `--json` are all clean usage
  errors (`2`) — never a silent last-wins. `--quiet` and `--lang` are no-ops for
  a machine format, exactly as with `--json`. See
  [`EXIT-CODES.md`](EXIT-CODES.md) for the full contract.
- `--quiet` — suppress the human `PASS`/`FAIL`/`Syntax-binding warnings` summary
  on stdout. The **exit code is unchanged**, and `--quiet` does *not* suppress
  `--json`: `validate --quiet --json` still prints the JSON (quiet only silences
  the human summary). It has no effect on `receipt`, whose canonical JSON *is*
  its output.
- `--profile=en16931|xrechnung` — select the rule set (default `en16931`).
- `--lang=en|de` — language of human-facing text only (default `en`), accepted on
  `validate`, `validate-batch` and `--explain`.
  Under `de` a rule that has an **official** German assert renders it; all other
  rules keep their English message. It changes nothing else — same rules fire,
  same offending element, byte-identical `--json`, same exit code. See
  [§German-language messages](#german-language-messages---lang-de) for the exact
  coverage.
- `--explain <RULE-ID> [--lang=en|de]` — **look up one rule id from a failure**. Prints the
  `remediation_catalog.json` entry — what the rule requires, the BT/BG business
  terms, the XML location hint, the one-line fix, the severity, and the official
  Schematron assert it is derived from — then exits. It reads no invoice,
  resolves no config and produces no verdict, so it is safe to run anywhere;
  lookup is case-insensitive. Exit `0` when the id is catalogued, `1` when it is
  not (a *lookup miss*, not a verdict — stdout stays empty so the block is safe
  to capture), `2` if the rule id is missing from the command line. This is the
  same code path as `python3 -m einvoice.report --explain <RULE-ID>` and prints
  byte-identical output; the flag exists on `einvoice` too because that is where
  you already are when a rule id appears in a `FAIL:` line. `--lang de` reads the
  block in German — the catalog's `title_de` / `fix_de`, the official KoSIT
  message on the rules that have one, plus a `german` line naming which of those
  two provenances you got; anything without a German string stays English. Both
  entry points accept it and stay byte-identical. Exact per-field coverage:
  [EXIT-CODES.md](EXIT-CODES.md#what---langde-actually-gives-you-measured-german-coverage).
- `--show-config` — **read-only observability**: resolve the effective
  `format` / `fail-on` / `lang` exactly as a real `validate` run would (explicit
  flag > config file > built-in default) and print each with its **source** —
  `flag`, the config filename (`.einvoice.toml` / `pyproject.toml`), or
  `default` — then exit `0`. It reads no input file and runs no validation; like
  `info` it writes only stdout, nothing on stderr on success. A misconfigured
  config file still errors (`2`) with the same message a real run gives — the
  resolution and vocabulary checks are shared, never re-implemented. Purely
  additive: omitting the flag leaves every `validate` run byte-identical to
  today. Pinned by `test_show_config.py`; the config-file layer it reports is
  documented in [`QUICKSTART.md`](QUICKSTART.md) §6.

**Input** — `validate <invoice.xml>` reads a file; `validate -` reads the
invoice XML from **stdin** (e.g. `curl -s … | einvoice validate -`). The stdin
bytes are staged to a temporary file and validated through the *identical*
DTD/XXE/resource-hardened parser used for on-disk files — piping in does **not**
get a relaxed parse path (see [`SECURITY.md`](SECURITY.md)). `receipt` reads a
file only.

**`receipt --verify <receipt.json>`** — the one-command integrity check for a
receipt `einvoice receipt` previously emitted. It re-hashes the receipt's
canonical body with the *exact same* canonicalizer that built it and compares to
the stored `content_sha256`, then reports the result. It validates nothing and
changes no verdict — it only re-hashes bytes already in the receipt:

```
$ einvoice receipt invoice.xml > receipt.json
$ einvoice receipt --verify receipt.json
VERIFIED: receipt.json
  content_sha256 = 6459697e0a75de9454eeac449a0c79f5a172945470fa6e1db4dbf49e6699b391
```

Exit codes reuse the existing taxonomy (no new code): `0` + `VERIFIED` when the
hash matches; `1` + `TAMPERED` (with the recomputed vs stored hash) when the
body no longer matches its hash — whether a field was altered or `content_sha256`
itself was corrupted; `2` + an `error:` line on stderr (no traceback) when the
file is not a readable receipt (non-JSON / garbage / truncated, valid JSON that
is not a receipt, or a nonexistent path). `--verify` is valid only for `receipt`.
This is a convenience over the manual recipe, not a stronger guarantee: it shares
the recompute-and-compare limit documented in
[`RECEIPT-VERIFICATION.md`](RECEIPT-VERIFICATION.md), which also gives the
zero-trust recipe for consumers who won't run our binary.

**Batch — `validate-batch <dir|glob>`** validates a whole set of invoices in one
run. The argument is **either a directory** (every `*.xml`/`*.pdf` invoice file
under it, recursively; dotfiles and dot-directories skipped) **or a shell-style
glob** (`invoices/*.xml`, or `'invoices/**/*.xml'` for a recursive match — quote
it so your shell doesn't pre-expand it). Every file goes through the **same**
hardened parser and rule engine as `validate`, so a hostile `DOCTYPE`/entity file
is reported as an `ERROR` (never parsed, never aborts the batch), and the
directory and glob forms produce **byte-identical aggregate counts** over the same
file set. It reuses the batch engine in `einvoice/report.py`
(`build_batch_report` / `build_batch_report_from_files` / `batch_exit_code` /
`build_batch_text`) verbatim — no aggregation or rule logic is re-implemented, and
each per-file report is byte-identical to validating that file on its own. That
means the per-file `violations[]` under `--json` carry **the same nine keys**
(`rule`, `message`, `element`, `severity`, `field`, `title`, `fix_hint`, `terms`,
`location`) as `einvoice validate --json`, so a CI job can run `validate` on a
pre-commit hook and `validate-batch` on a nightly sweep with **one** parser —
`element` was the one key the batch records were missing before 0.2.7, and
`test_json_surface_parity.py` now compares the two surfaces so it cannot recur.
Output
is a per-file `PASS`/`FAIL`/`ERROR` line **plus the rule ids behind its counts**,
an aggregate tally, a "most violated rules" ranking and one
`einvoice --explain <RULE-ID>` pointer — or the aggregate
`einvoice-conformance-batch/v1` dict with `--json`; `--quiet` suppresses
the human summary but keeps the exit code (and still emits JSON under `--json`).
Concretely, `einvoice validate-batch --profile xrechnung examples/01-missing-fields`
prints

```
FAIL  examples/01-missing-fields/broken.xml  2 fatal, 0 warning
  3 finding(s) total: 2 fatal, 1 non-fatal (--format json carries every field of each)
    [fatal] BR-DE-2: The group 'SELLER CONTACT' (BG-6) must be transmitted.
    [fatal] BR-DE-15: The element 'Buyer reference' (BT-10) must be transmitted.
    [information] BR-DE-TMP-32: The invoice should state the delivery/service date: …
PASS  examples/01-missing-fields/fixed.xml  conformant
  1 finding(s) total: 0 fatal, 1 non-fatal (--format json carries every field of each)
    [information] BR-DE-TMP-32: The invoice should state the delivery/service date: …

2 files: 1 passed, 1 failed  (2 fatal, 0 warning across all files)

Most violated rules (rule id, files affected):
  BR-DE-TMP-32  2 files
  BR-DE-2       1 file
  BR-DE-15      1 file

Explain any rule above: einvoice --explain BR-DE-2
```

Two honest limits on that listing. Each file lists at most **11** findings
(`einvoice.report._BATCH_RULE_LIST_CAP`, the batch twin of the single-file
report's 10-plus-headline budget) and then says
`... N more not shown — use --format json for all N`; the "most violated rules"
block is bounded by the same number and discloses the same way. And note a file
can `PASS` and still list findings — `fixed.xml` above is conformant, but the
advisory `BR-DE-TMP-32` is still worth seeing. An
**empty directory or zero-match glob** is reported honestly as `file_count: 0`
with a `note`, exit `0` — never a traceback. The **exit code** follows the
documented report precedence (fatal outranks parse): `0` when every file passes,
`1` if **any** file has a fatal violation, `3` if some file only errored
(not-well-formed / unsupported container) and none had a fatal. Pinned by
`test_cli_batch.py`.

**Build introspection — `einvoice info`** reports what *this* build contains,
read-only: no input file, nothing validated, exit `0` (extra arguments are a
usage error, exit `2`). Human form is stable `key: value` lines; `einvoice info
--json` emits the same payload as **one** sorted-keys JSON object with exactly
the keys `version`, `profiles`, `formats`, `rule_count`, `coverage` and
`attestation_sha256`. Every value is read or recomputed **at runtime** from the
package and its committed artifacts — the packaged `__version__`, the `PROFILES`
tuple, the `einvoice.report.REPORT_FORMATS` constant, the committed coverage
matrix, the live syntax-binding evaluator plus its catalog totals, and
`attestation.json`'s `content_sha256` — never retyped literals, so the output
can't silently drift from the build (pinned by `test_info.py`). Example:

```
python3 -m einvoice info --json | python3 -m json.tool
```

A tested fail-fast CI recipe built on this output — assert a required
profile/format in one line before your validate step — is in
[`QUICKSTART.md`](QUICKSTART.md) §5 (pinned by `test_ci_capability_recipe.py`).

**`--json` shape** — the exact field-by-field schema of the `--json` result
(including the `syntax_bindings` array and its two count fields) is documented
in [`REPORT-SCHEMA.md`](REPORT-SCHEMA.md). A machine-readable JSON Schema
(`report.schema.json`) for this shape is planned; once published it will live
alongside `REPORT-SCHEMA.md`, and this contract is written to stay compatible
with it — the CLI does not depend on that file existing.

### German-language messages (`--lang de`)

`--lang de` swaps the language of the human-facing failure message **only**.
Which rules fire, the offending element, the `--json` payload and the exit code
are all unchanged — the JSON output is byte-identical with or without the flag
(pinned by `test_lang.py`). The coverage is deliberately narrow, and stated here
without spin:

- **Exactly 50 rules carry an official German message**, surfaced by
  `--lang de`. These are precisely the `remediation_catalog.json` entries tagged
  `de_source == "kosit"` — the rules whose vendored KoSIT XRechnung
  `<sch:assert>` text is *itself German* (the `BR-DE-*` / `BR-DE-CVD-*` /
  `BR-TMP-*` / `BR-DEX` German-authored family). `test_lang.py` pins that count
  and re-extracts each German string from the vendored `.sch` on disk to prove
  it is genuinely present, not fabricated. The number here is the count of
  `message_de` fields in the catalog; a build gate fails if the prose and the
  catalog ever disagree, so it cannot silently drift.
- **That German text is lifted VERBATIM** from the vendored KoSIT XRechnung
  Schematron (`corpus/xrechnung-schematron/schematron/{ubl,cii}/XRechnung-*-validation.sch`),
  byte-for-byte, each `message_de` tagged with the `{artifact, assert_id}` it
  came from. We do **not** machine-translate and do **not** hand-translate any
  error text: a `message_de` is only ever the official KoSIT assert as KoSIT
  wrote it.
- **Every other rule is English-only by design.** The EN 16931 core
  (`BR-*`, `BR-CO-*`, `BR-CL-*`) and the other non-German-sourced rules (e.g.
  the English-authored PEPPOL asserts) have **no** official German assert text
  to quote, so under `--lang de` they fall back to their English message
  unchanged. This is silence-with-reason, not a coverage gap: showing the
  authoritative English assert is more honest than inventing a German one. There
  is **no machine translation** anywhere in this surface.

This official-German message is a **CLI / report human-message** feature: it is
not injected into `--json`, and this task added no field to the generated
`www/` rule pages. (Those bilingual rule pages already render the catalog's
German `title`/`fix` verbatim from the same source, each honestly labelled by
its `de_source` as either the official KoSIT assert or a clearly-marked
translation — pre-existing behaviour, not part of the `--lang de` message
surface.)

### Syntax-binding findings (`syntax_bindings`)

Alongside the EN 16931 / XRechnung `BR-*` business rules, the two vendored CEN
preprocessed Schematron artifacts also carry **syntax-binding** asserts
(`UBL-CR-*`/`UBL-DT-*`/`UBL-SR-*` for UBL, `CII-DT-*`/`CII-SR-*` for CII) — pure
syntax-layer restrictions such as "this element MUST NOT appear", "at most one of
X", or a decimal-place cap, *not* semantic business rules. A restricted,
data-driven evaluator (`einvoice/syntax_binding_eval.py` — a closed XPath subset,
not a general processor) mirrors **735 of 756 UBL + 506 of 583 CII** of them, each
differential-proven equivalent to the official CEN Schematron at **0 divergences**
over the corpus; the remaining **98 (21 UBL + 77 CII)** are left machine-listed as
`known-open` in [`COVERAGE.md`](COVERAGE.md) — never guessed, never silently
dropped. These counts are **kept strictly separate** from the 297 business rules
and are recomputed live by `test_syntax_binding.py`.

`einvoice validate --json` surfaces the UBL findings under a distinct
`syntax_bindings` array (each with `id`, `category` `"syntax-binding"`,
`severity`, `flag`, `message`, `element`), plus `syntax_binding_fatal_count` and
`syntax_binding_warning_count`. All implemented ids carry the official
`flag=warning`, and this category is **advisory only**: a syntax-binding finding
**never** changes `valid` and **never** changes the exit code — the `0`/`1`
verdict above stays driven solely by fatal `BR-*` violations. The human summary
adds one line, `Syntax-binding warnings: N`. This array carries **UBL ids only**
— not because CII is refused (the CLI grades a raw `CrossIndustryInvoice` on its
business rules like any other document, see [`EXIT-CODES.md`](EXIT-CODES.md)) but
because `report.syntax_binding_section` runs only the implemented UBL entries, so
a CII document validates normally and reports `syntax_bindings: []`. The CII
binding's own asserts are evaluated by `einvoice.syntax_binding_eval.cii_fired_ids`
and differentially proven by the `differential.py sbcii` leg. Full field docs:
[`REPORT-SCHEMA.md`](REPORT-SCHEMA.md).

### Reproduce this yourself

The whole "machine-proven-coverage" claim rebuilds from the vendored official
artifacts with **one** committed command — from a **repository checkout**, not
from the wheel (`prove.py`, `differential.py` and the corpus are repo-only, by
design):

```sh
PYTHONPATH=$HOME/.local/lib/python3.10/site-packages python3 prove.py
```

[`prove.py`](prove.py) runs, end to end and exiting non-zero on any failure:

1. [`differential.py`](differential.py) over **all six legs** — for every one of
   our implemented rule and syntax-binding ids it asks the same yes/no question
   ("does rule *R* fire on this document?") of the OFFICIAL vendored CEN / KoSIT
   Schematron (via Saxon) and of our engine, across the whole corpus, and counts
   **divergences** (a false positive or a miss). `prove.py` asserts and echoes
   that this count is **0**.
2. [`conformance.py`](conformance.py) — the targeted invalid/valid fragment
   corpus — and asserts **0 hard fails** (no false positives; every covered
   invalid vector detected with the correct rule id).
3. the coverage headline, recomputed **live** this run and printed in the shape:

   > *N* business rules / **0 divergences** across all differential legs / *U* of
   > *Ut* UBL + *C* of *Ct* CII syntax-binding asserts differential-proven per
   > binding

`prove.py` reads no headline number from a string literal. What each field means
and where it comes from:

- **business rules** — `coverage_matrix.json['rule_count']`, the EN 16931 +
  XRechnung/Peppol `BR-*` rules our engine implements. Independently re-checked
  by [`test_coverage_matrix.py`](test_coverage_matrix.py) (the JSON is a live
  re-render of what the engine fires, and `COVERAGE.md` is a byte-identical
  render of the JSON).
- **divergences** — the step-1 total; **0** means our verdict never departs from
  the official Schematron over the corpus. This is the differential harness's own
  `OVERALL DIVERGENCES ACROSS LEGS` line.
- **U of Ut UBL / C of Ct CII** — the per-binding syntax-binding asserts our
  restricted evaluator proves equivalent (`differential.py` LEG 5 `sb` / LEG 6
  `sbcii`). The totals come from `einvoice.syntax_binding.accounting()` and the
  proven counts from `einvoice.syntax_binding_eval.implemented_ids()` /
  `cii_implemented_ids()` — the **same** recomputation
  [`test_syntax_binding.py`](test_syntax_binding.py) asserts.

Because it re-runs the full differential (all legs) plus the conformance corpus,
`prove.py` takes a few minutes. It always pays that full cost: `prove.py` sets
`DIFF_NO_CACHE=1` for every child it spawns, bypassing the local proof cache
that speeds up ordinary `differential.py` gate runs, so a reproduce run is
always a fully live Saxon re-proof — even on a warm development machine. [`test_prove.py`](test_prove.py) runs it and
asserts it exits 0, prints `0 divergences`, and that the UBL / CII / rule numbers
it printed equal a fresh independent recompute — so the entrypoint is verified to
report live truth, never a frozen string.

### Independently verify our conformance claim

`prove.py` above rebuilds the claim from scratch (a few minutes, needs Saxon).
If you instead want a **fast, single-command check that the published claim has
not drifted from the committed source**, run this — again from a **repository
checkout**, since it re-derives the numbers from the source tree (the wheel
carries the resulting `attestation.json`, not the verifier script):

```sh
python3 verify_attestation.py
```

This reads [`attestation.json`](attestation.json) — a byte-reproducible record
that pins the exact numbers we publish — and confirms they still match the live
source tree. It exits `0` only if nothing has moved. `attestation.json` pins:

- **297 business rules** asserted by the engine (the frozen `rules.count`);
- the frozen syntax-binding coverage headline: **741 of 756 UBL** + **554 of
  583 CII** syntax-binding asserts differential-proven per binding;
- the in-scope KoSIT test-suite pass rates: **39 of 39 UBL** and **39 of 39
  CII** official documents accepted;
- the SHA-256 of each of the **3** vendored official corpora (CEN EN 16931
  1.3.16, XRechnung Schematron 2.5.0, XRechnung test-suite).

`verify_attestation.py` runs two independent checks and exits **non-zero on any
drift**: (1) it regenerates the attestation fresh from the four live source
artifacts (`export/rules.json`, `export/coverage.json`,
`testsuite_conformance.json`, `sbom/bom.json`) and requires it byte-for-byte
equal to the committed `attestation.json` — so changing any recorded number (a
rule count, a coverage figure, a pass rate, or a recorded corpus hash) fails the
byte compare; (2) it re-walks each vendored corpus tree and requires its digest
to equal the pinned SHA-256 — so editing a corpus byte without regenerating the
attestation is caught too. A skeptical ERP or procurement evaluator can run this
against the source and confirm the conformance claim for themselves.
[`test_attestation.py`](test_attestation.py) asserts this command exits 0, that
every number stated here equals the value in `attestation.json`, and that the
tamper paths fail closed.

---

## 4. CI conformance gate

The distribution artifact an ERP/billing vendor actually wants: a build gate
that makes "a non-conformant invoice reached the repo" a **red build**, with
the violated rule ID named in the job log. Everything lives in
[`ci/`](ci/README.md):

- [`ci/validate-invoices.sh`](ci/validate-invoices.sh) — the gate. POSIX sh,
  zero deps beyond python3. Exit `0` = all conformant; exit `1` = at least one
  invoice failed (rule IDs printed); exit `2` = the gate itself is
  misconfigured — including **finding no invoices at all** (an empty gate is a
  broken gate, opt out with `EINVOICE_ALLOW_EMPTY=1`).
- [`ci/github-actions.yml`](ci/github-actions.yml) — copy to
  `.github/workflows/invoice-conformance.yml`.
- [`ci/gitlab-ci.yml`](ci/gitlab-ci.yml) — merge into your `.gitlab-ci.yml`.

**Zero-install alternative (GitHub only): the committed composite Action.**
Instead of copying a workflow and pip-installing a vendored copy, pin the
Action that ships in this repo at [`action/`](action/README.md) — the
zero-dependency validator travels *inside* the pinned ref, so there is nothing
to install, and it additionally merges per-file SARIF for
`github/codeql-action/upload-sarif` → inline PR annotations:

```yaml
- uses: verifyhash/verifyhash/einvoice/action@<ref>   # pin a full commit SHA you trust
  with:
    path: invoices/
    fail-on: fatal
```

It is a thin wrapper around the same `python3 -m einvoice.report` entrypoint
(no second engine, no new rules) and is gated by `test_action.py`. Full inputs
(`path`, `format`, `fail-on`, `sarif-file`, `profile`), the `fail-on` →
exit-code mapping, and the SARIF-upload step live in
[`action/README.md`](action/README.md). It is referenced by in-repo path as
shown — it is **not** listed on the GitHub Marketplace.

The 60-second version (any CI system). This one vendors a **repository
checkout** into `third_party/` — `ci/validate-invoices.sh` is a repo file, not
part of the wheel; if you would rather install from PyPI, use
`python3 -m pip install verifyhash-einvoice` and the recipes in
[`ci/README.md`](ci/README.md):

```sh
python3 -m pip install ./third_party/einvoice        # vendored copy; zero deps
sh third_party/einvoice/ci/validate-invoices.sh invoices/
```

and a failure looks like:

```
FAIL: invoices/2026-04-017.xml
  BR-DE-15
  JUnit: einvoice-junit/3_invoices_2026-04-017.xml.junit.xml
conformance gate: 1/12 invoice(s) NON-CONFORMANT (profile=xrechnung) — FAIL
```

(The gate script lists **rule ids only** and points at the per-invoice JUnit
file, which carries the message and the offending XPath in its `<failure>`. The
richer human summary — message, offending element, `--explain` and rule-page
lines — is what `einvoice validate <file>` prints; see §3.)

The next question is always *what is `BR-DE-15` and what do I change?* — so ask
the same tool, no invoice and no repository checkout required:

```sh
einvoice --explain BR-DE-15
```

It prints the rule's `requires` sentence, the BT/BG business terms, the XML
`location` to look at (`cbc:BuyerReference` here), the one-line `fix`, the
severity, and the original Schematron assert. Exit `0` when the id is
catalogued, `1` when it is not. Identical output to
`python3 -m einvoice.report --explain BR-DE-15` — one implementation, two entry
points — and `REPORT-FORMATS.md` documents the mode in full.

Same honest scope as §2: the gate proves your invoices pass the
**implemented** rule set (219 core + 55 German-layer rules — the
authoritative list is `COVERAGE.md`), not the full standard. The gate's behaviour
(fails naming the rule ID, passes conformant sets, refuses empty input) is
itself under test in `test_packaging.py`.

### Machine-readable report (`python3 -m einvoice.report`)

When a CI step needs the outcome as **structured data** (to archive, diff, or
feed a dashboard) rather than a log line, `einvoice.report` emits a single
**versioned JSON** document to stdout and mirrors the same exit-code contract
(`0` clean, `1` fatal violation, `3` not-well-formed XML):

```sh
python3 -m einvoice.report --profile xrechnung invoices/2026-04-017.xml
# {"report_version":1,"schema":"einvoice-conformance-report/v1",...,
#  "valid":false,"fatal_count":1,"violations":[{"rule":"BR-DE-15",...}]}
```

Every violation record carries exactly `rule`, `severity`, `message`, `field`;
add `--pretty` for indented output. It re-uses `einvoice.validate` verbatim
(no rule logic of its own, zero deps). The full field-by-field contract and
its `report_version`/`schema` versioning semantics are documented in
[`REPORT-SCHEMA.md`](REPORT-SCHEMA.md) (and mirrored in the `REPORT_SCHEMA`
constant of `einvoice/report.py`).

#### Batch / folder validation

Point the same command at a **directory** (or pass `--recurse`) to validate a
whole folder of invoices in one run. Every `*.xml` / `*.pdf` file under the
directory is walked recursively (dotfiles and dot-directories such as `.git`
are skipped), the file list is sorted for deterministic output, and each file
is validated through the **exact same** `build_report` — this is a wrapper, not
a second engine.

```sh
python3 -m einvoice.report --profile xrechnung invoices/
# {"report_version":1,"schema":"einvoice-conformance-batch/v1","root":"invoices/",
#  "file_count":42,"fatal_count":3,"warning_count":11,"failed_file_count":2,
#  "files":[{...single-file report...}, ...]}
```

The aggregate document uses its **own** independently-versioned schema id
(`einvoice-conformance-batch/v1`) and wraps the per-file reports **unchanged** —
each entry in `files` is byte-for-byte identical to validating that file on its
own. It carries the summed `fatal_count` / `warning_count` / `violation_count`
plus `failed_file_count` (files that errored or hold a fatal).

Aggregate exit code (documented precedence — **fatal outranks parse**):

- `0` — every file passed (each `fatal_count == 0`, no error), **or** the
  directory held no invoice files (reported honestly as `file_count: 0` with a
  `"no invoice files found"` note — never a fake pass, never a traceback);
- `1` — at least one file has a **fatal** violation;
- `3` — at least one file **errored** (not-well-formed XML / unsupported PDF
  container) and no file had a fatal violation.

Batch mode supports `--format json` (default, `--pretty` for indented),
`--format junit` (an aggregate `<testsuites>` with one `<testsuite>` per file)
and `--format text` (a status line per file, the rule ids behind its counts, a
"most violated rules" ranking and an `--explain` pointer). `--format
sarif/html/badge` validate a *single* file and are rejected on a directory with
a clear error. Single-file invocation is completely unchanged.

**This entry point is no longer the only route to those formats.** The
`einvoice` console script accepts `--format <fmt>` on `validate` and
`validate-batch` too (see the flag list above), and for the seven delegated
formats — `junit`, `sarif`, `gitlab`, `github`, `azure`, `html`, `badge` — both
surfaces call the same emitter, so those bodies are byte-identical for the same
invoice and profile (measured). The two `json` documents and the two `text`
summaries are *not* interchangeable: each surface keeps its own long-standing
shape (`einvoice validate --json` is the CLI result object; `einvoice.report`
emits the versioned `einvoice-conformance-report/v1` document), and `--format`
deliberately changes neither. Reach for
`python3 -m einvoice.report` when you want what only it offers: `--pretty`
(indented JSON) and `--recurse` (an explicit directory walk; the console script
spells that `validate-batch`). Two capabilities this list used to name have
since moved onto the console script and are no longer reasons to leave it: the
PDF-container dispatch that extracts the XML from a Factur-X/ZUGFeRD file, and
`--baseline` — `einvoice validate --baseline prev.json invoice.xml` fails on a
*new* fatal versus a stored report, delegating to this module's diff so both
entry points emit the same `einvoice-conformance-diff/v1` document.
Mind the one deliberate difference: this module defaults to `--profile
xrechnung`, the console script to `en16931`.

### Use as a pre-commit hook

The CI gate above catches a bad invoice after it is pushed. To catch it one step
earlier — at the commit itself — this repository ships a **provider-side hook
manifest** at the repo ROOT
([`.pre-commit-hooks.yaml`](../.pre-commit-hooks.yaml)) that
[pre-commit](https://pre-commit.com) resolves **remotely**. Any repo can adopt
the check with a `repos:` reference; nothing is copied or vendored in.

Add this to **your** repo's `.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/verifyhash/verifyhash
    rev: main              # no release tags exist yet; for a reproducible
                           # build pin a full 40-char commit SHA from
                           # github.com/verifyhash/verifyhash/commits instead
    hooks:
      - id: einvoice
```

then run `pre-commit install` once. On every commit, pre-commit clones this
repo at `rev`, reads the root manifest, and runs the `einvoice` hook over the
`*.xml` you staged. A fatal EN 16931 / XRechnung violation blocks the commit and
prints the offending rule id(s) — the same outcome CI would produce, because the
hook drives the **same `python3 -m einvoice.report` entrypoint** (no second
validator, no new format). It stays inert on any commit that stages no XML.

**Prerequisite — the one thing you must install.** The manifest deliberately
declares `language: script`, not `language: python`. The validator is the
`einvoice` package under this repo's **`einvoice/` subdirectory** (its
`pyproject.toml` sits at `einvoice/pyproject.toml`, not the repo root), so
`language: python` would make pre-commit pip-install the repo ROOT — the
Node/Hardhat project, which is not the package — and fail. `language: script`
runs the committed wrapper (`einvoice/ci/pre-commit-einvoice.sh`) directly and
installs nothing, which means the `einvoice` validator must already be
importable in the environment `pre-commit` runs in. Install it once:

```sh
python3 -m pip install verifyhash-einvoice
# or, from a checkout / vendored copy:
#  python3 -m pip install /path/to/einvoice
```

(or export `EINVOICE_CMD` / `PYTHONPATH` to point the wrapper at a source
checkout). If the validator is not importable the hook exits `2` with an install
hint rather than passing silently — a broken hook fails loud, never quiet. Honest
limit: because the hook reuses the validator, its scope is the validator's scope
(§2) — a passing commit means "no **implemented** rule fired", not full legal
EN 16931 conformance.

## 5. Intended revenue model

If this continues past the first slice, the model is boring on purpose: a
small, flat, self-serve **commercial license** for vendors (ERP, billing,
e-invoicing platforms) who embed the validator in their product. Two prices,
one-time, no contract and no sales call:

- **$29** — single developer: email support, rule-corpus update notices, and
  a ready-made vendor key.
- **$290** — whole vendor team: the same, for an entire company, with priority
  on support and update notices.

The license sells **convenience, not permission**: Apache-2.0 already lets
anyone embed the engine in a closed-source product at no cost, so this is
never a usage fee, never a compliance requirement, and never metered by
request volume.

Explicitly ruled out, permanently: **no token, no coin, no on-chain payment
instrument of any kind.** Nothing about invoice validation needs one, and
this project will never fund itself by selling one.

No customer today, and nothing has been sold. This section exists so the
incentive structure is on the record before the first conversation with a
vendor, not after.

---

## 6. KILL / CONTINUE metric

A first slice earns further investment or it doesn't. The signal, timeboxed:

**CONTINUE** if, within **90 days** of this README, at least one of:

1. **One ERP/billing/e-invoicing vendor agrees in writing to pilot or embed**
   the validator (even unpaid) — evidence that "embeddable, zero-dependency,
   auditable" is a real wedge against the incumbent Java/Schematron stacks; or
2. The validator reaches **60+ implemented rules including the full
   `BR-DE-*` set, passing the full KoSIT XRechnung test suite** (not just the
   curated 32-vector corpus) with zero false positives — evidence the rule
   engine scales to real coverage without an architecture rewrite.
   **→ MET.** (The threshold was written for a 20-rule first slice; it is kept
   here verbatim as the record of what was actually promised. The engine now
   asserts **297 business rules**, ~5x the bar, with the full `BR-DE-*` set
   differential-proven against the official KoSIT artifact — see the status
   paragraph below.)

**KILL** if neither happens: write up what was learned, archive the repo, and
stop. The corpus and harness remain useful artifacts either way.

Current status against this metric: **297 business rules** asserted by the
engine (`python3 -m einvoice info --json` → `rule_count`; the per-rule
inventory is [`COVERAGE.md`](COVERAGE.md)). That covers the EN 16931 core —
**219 implemented + 4 excluded + 0 missing = 223 official `BR-*` asserts**
against each vendored CEN artifact, the same split in the UBL and the CII
universe — plus the XRechnung CIUS/CVD/extension layer and the 21
KoSIT-vendored `PEPPOL-EN16931-R*` rules. Each batch is differential-proven at
100% against its official Schematron on both the UBL and the CII leg;
fireable-missing = 0 in both CEN universes, with the 4 official `test="true()"`
tautologies `BR-CO-05`–`08` documented as the only never-provable ids. Vendors
contacted: 0. Metric #2 is therefore MET with room to spare; metric #1 (a
vendor) is now the whole game.

---

## Licensing

Everything in `einvoice/` is open source under
[Apache-2.0](../LICENSE) — free for everyone, including commercial use and
integration into closed-source products, subject to the usual Apache-2.0
conditions: keep the license text and the attribution in
[`NOTICE`](NOTICE) with redistributions, and mark changed files.

Because Apache-2.0 already permits closed-source embedding for free, the
optional **commercial license** does not sell permission or "compliance" —
it adds **support, prioritised rule-corpus update notices, and vendor-key
convenience** at a flat, self-serve price:

- **$29** — single developer.
- **$290** — whole vendor team (any number of developers).

Both are one-time, with no contract to negotiate and no sales call. Buy or
ask via **hello@verifyhash.com** (the private commercial contact); the
self-serve page is the licensing page
([`www/licensing/`](www/licensing/index.html), published at
<https://verifyhash.com/einvoice/licensing/>). The checkout link is bound at
deploy from a single `CHECKOUT_URL` placeholder in `gen_site.py`; until it is
set the page shows an honest "checkout opening shortly — email
hello@verifyhash.com" line rather than a dead link. You never need the
commercial license to use, embed or redistribute the engine.

---

## Sources / licenses

- EN 16931 corpus & Schematron: `github.com/ConnectingEurope/eInvoicing-EN16931` (EUPL-1.2), vendored under `corpus/cen-en16931/`.
- XRechnung test suite: `github.com/itplr-kosit/xrechnung-testsuite` (Apache-2.0), vendored under `corpus/xrechnung-testsuite/`.
- XRechnung Schematron v2.5.0 (XRechnung 3.0.2): `github.com/itplr-kosit/xrechnung-schematron` (Apache-2.0), vendored under `corpus/xrechnung-schematron/` (see its `VENDORED.md`).
- Rule text quoted verbatim from the vendored `EN16931-model.sch` / `EN16931-syntax.sch` / `XRechnung-UBL-validation.sch`.
