You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The business rule pack of EN 16931-1:2026, rules/en16931-2026/0.1: 252 rules, of which 199
are those of clause 6.4 (194 implemented, 5 not decidable, each with its reason), the fraction
digit rules of Table 28 and the code list rules. No official validation artefact is published
for the edition, so every rule names in oracle what stands behind it — downgrade for the
rules the edition leaves unchanged, measured against the Schematron of release 1.3.16 over the
document written back with esj upgrade, cases for the rules it changed or adds — and a
document of that edition validates INVALID or INDETERMINATE, never VALID, with the new
cause no-artefacts-for-edition (conformance/rules-2026/coverage.md, ledger.md). The pack is separable with the edition.
derive() for the typed view of EN 16931-1:2026: …typed.v2026.Totals.of(minorUnits) writes
the line net amounts, one VAT breakdown per category, rate, exemption reason and goods/services
code, and the totals, with the third party charges (BG-34) added into the amount due.
Each amount is rounded once, half up, to the minor unit of its currency; MinorUnits of esj-core holds those numbers and En16931V2026.minorUnits() reads them from the pack's
currency snapshot. A currency the snapshot gives no minor unit is refused, never guessed; a
rule of the pack that asks for one reports that it was not decided. The
pack writes out BR-CO-10, -11, -12 and -15, which round to the same minor unit.
The rule language has three more operators — minorUnit, unit and atRoot — and the scale
of round and of decimals may be an expression; decimals applies to every numeric term.
A rule may name in undecided a case a document may state and the rule has no answer for;
there it reports that it was not decided, at info, instead of holding silently — the 2026
pack does so for a price base quantity of zero in BR-CO-32 and BR-67.
A pack manifest names its edition, which the engine checks against the registry, may take
rules of another pack over by identifier (shares) and read a code list snapshot of another
pack ({"day", "from"}). The TypeScript and C# bindings read all of it and run the 2026 pack;
the fixture manifest carries its cases in the part of that edition.
esj validate runs the pack of the edition a document names. The row, the JSON report
(rules.pack.edition, rules.pack.corroborated) and the report file name the pack and, for
the 2026 one, say it is not corroborated by an official artefact. esj --list-packs lists
the rule packs with their edition and the count of rules per oracle. esj upgrade --to 2026
evaluates a decimal bound over the minor unit of the currency a value is written in — BT-5,
BT-6 for BT-111, BT-184 in a VAT breakdown — against the currency snapshot of that pack
(UpgradeOptions.minorUnits) and reports a value beyond it, unrounded.
The container image is published with every release at ghcr.io/bsnsoft/esj, for linux/amd64
and linux/arm64, tagged with the version and latest
(docs/install.md).
esj convert --to cii|ubl --fail-on-loss: where the target syntax has no place for part of the
document, nothing is written — neither to --out nor to the standard output — and the run
leaves with exit code 8. The exit code table did not grow: 8 was reserved for exactly this
case and is now used, and every printing of the table says so. A value written by convention
and a term left behind by design are not losses. Without the option a loss is warned about and
the exit code stays 0 (docs/cli.md).
The CI runs the TypeScript and C# bindings on every push, in the job bindings: npm ci and npm test, dotnet test, and the fixture runner over each binding.
The fixture manifest pins division in the rule language for every implementation: the section arithmetic runs the pack conformance/fixtures/arithmetic/pack.json, whose quotients are
worked out by hand, and a rules request of the runner may name that pack. The C# binding
reads a pack from a stream with RulePacks.Read.
Changed
Every rule of a pack, and every entry of javaRules, which is now an object {class, oracle},
names its oracle; the rules of en16931/1.3.16 say artefact. verifiedAgainst is optional.
A pack manifest written for 0.9.x — without edition, with a rule that names no oracle, or
with a javaRules entry written as a string — is refused by RulePacks.read. no-pack-for-edition now means only that this build carries no pack for the edition.
The writer's report tells a term left behind by design from a loss. Handed the extension
registries of a document (WriterOptions.builder().extensions(...), and esj convert and esj validate hand it the ones --extension loads), the writer notes a value of a term whose
registry declares "transport": "none" — the B2C extension's — as TERM_BY_DESIGN with the
registry in WriteNote.registry(), and counts it neither as written nor as dropped; WriteReport.byDesign() gathers those terms by registry. esj convert --extension b2c names
them on one information line instead of warning that ten values were not written, and the
JSON report carries the registry beside each such note. esj validate reaches the same rows
and exit codes as before.
The letter layout is the default of esj render and of PdfRenderer
(RenderOptions.DEFAULT_LAYOUT), for a template that names none as well; --layout generic, Layout.GENERIC or "layout": "generic" ask for the generic layout, in which the PDF
validation report still draws the invoice it carries.
Letter layout: a page after the first begins as far under the rule of its compact head as the
first section of page one begins under the rule of the title, whatever opens the page.
Letter layout: the seller's details in the foot of page one are written value first — an
identifier followed by the code of its scheme, or by a short word where the document states
none (HRB 12345 (Registernummer)), and the contact point without a label.
Letter layout: the preceding invoices of BG-3 stand under the title, up to three, with their
dates; a credit note (the types the display names call one) labels its number, its date and
its closing amount as a credit note's; neither a credit note nor a self-billed invoice carries
an EPC QR code, since its reader is not the one who pays (docs/letter-layout.md).
The libraries underneath moved on: Saxon-HE 13.0 runs the validation artefacts and the
stylesheets, where 12.10 did, the tests run on JUnit 6, and the PostgreSQL that docs/storage.md is executed against is release 18 on every platform the
build runs on, pinned through the bill of materials of its binaries rather than left to the
default of the artefact that starts it. Nothing observable changed: the ledgers, the fixture
manifest and the checked-in renderings hold as they are.
Fixed
Division in the TypeScript binding is exact where the quotient terminates, as rules/README.md
says: Decimal.divide computed every quotient to 34 fraction digits and stripped the zeros, so
a terminating quotient that needs more digits was rounded — 1E-35 / 1 came out as 0. The
Java and C# implementations were measured and already divided exactly.
A row of a table that fits on a page no longer sends the lines hanging under it to the next
page under a carry-over line: the room for that line is asked for only where the lines go over.
The footer of the PDF validation report no longer runs into the page count for a long input
path: the path is shortened in its middle, keeping its beginning and its file name.
A hybrid PDF with two embedded files under one name is refused instead of validated on the one
a map kept: both are listed, the name is reported (PDF-EMBEDDED-DUPLICATE-NAME), and files a
page refers to count as attachments too (docs/pdf-input.md).
The output intent of a rendering carries the vendored sRGB profile byte for byte on every
runtime. PDFBox was handed the profile as a stream, read it into a java.awt.color.ICC_Profile
and embedded what that object gave back, and on a runtime whose colour management
re-serializes a profile it was handed — the Ubuntu build of OpenJDK 21, for one — the header
of the embedded profile named that engine as the preferred CMM, so the file was no longer the
ICC's and the rendering no longer the checked-in one. The stream is now filled from the file
itself; the checked-in renderings are unchanged.
The checksum beside the platform-independent archive of a release, esj-<version>.zip.sha256,
is the checksum of that archive. Each of the three packaging jobs of the release workflow
uploaded the archive and its checksum under the same two names, and the release kept whichever
copy of each came last — for 0.9.1 the archive of one job and the checksum of another, which
does not match it; the six platform archives match theirs. The linux-x64 job alone delivers
it now, its entries carry fixed times, modes and order, so that the clock, the time zone and the
umask of a build leave no trace in its bytes, and nothing is attached before every checksum has
been checked against the archive beside it, no name comes from two jobs with different
contents, and a tag finds the archive of the version it names.
esj embed and esj render --embed cii hand the writer of the attachment the extension
registries --extension loads, as esj convert does: a term whose registry declares "transport": "none" is named on one information line instead of being warned about as a value
that was not written. EmbedOptions.withExtensions(...) hands them over in Java; the hybrid
file is the same file.
conformance/fixtures/manifest.schema.json describes the rule case file as it is written: it
required a part of every file, and the rule case file names its pack, the directory of that
pack and its cases instead. FixtureManifestTest validates every file of the manifest against
the schema.
dist/package.sh --out <directory> takes a relative directory: zip left with exit code 15,
and linux-native and smoke looked for it under the repository rather than under the
working directory.