Skip to content

Releases: bsnsoft/esj

EN 16931 Semantic JSON 1.0.0-rc.4

Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Oct 15:22
Immutable release. Only release title and notes can be modified.

Release candidate 4 of 1.0.0 — the software of rc.3, whose tag has no release (its macOS archive did not build), with the fix below. Format 1.0 (see rc.1 and rc.2 in CHANGELOG.md).

Fixed

  • ESJ Viewer.app is built on the macos-15 runner again: the script that turns the compiled
    AppleScript into the viewer removed a property-list key the applet template of that macOS does not
    write, and stopped. Every key it sets or removes is now handled whichever keys the template
    carries. 1.0.0-rc.3 has no release for that reason; its tag exists, nothing of it was published.

Changes since 1.0.0-rc.2 (the section of 1.0.0-rc.3)

Added

  • esj view <file> shows an invoice in the browser: one page in a directory of its own, with
    the facts of the invoice and the verdict at the top and a tab each for the PDF as it came, the
    letter esj render draws from its data, the KoSIT rendering and, with --report, the
    validation report, kept beside the page as an HTML file and a PDF; every file is the bytes its
    own command writes. Every outcome is a page — a PDF without an invoice, several candidates (a
    table with the command that opens each), ZUGFeRD 1.0, an encrypted PDF, a file that is not
    read, a limit — saying what the file is and what to ask the sender for, with the exit code
    render leaves with, or with --report validate. The page runs no script of its own (the
    tabs are CSS), fetches nothing and says so in a content security policy. --out <dir>,
    --lang (the default from the locale), --template, --no-payment-code, --output json;
    --text prints the invoice in the terminal instead, and with --report the lines of
    esj validate under it (docs/view.md).
  • esj render --text writes the invoice as plain text for a terminal: the facts and names of the
    letter layout top to bottom, the lines as a table, UTF-8 without colour or box drawing, in
    --width columns (default COLUMNS where it is 60 or more, otherwise 100), no line longer and
    nothing cut (docs/cli.md).
  • The deb and the rpm install a desktop entry, ESJ Viewer, that offers esj view --report under
    Open with for XML and PDF files, with its icon; the macOS archive carries ESJ Viewer.app
    beside the executable, an alternate viewer of the same two types, signed ad hoc;
    docs/install.md gives the line that does it
    on Windows. None of them makes itself the default program of a type.

Changed

  • The macOS executable and its archives are built on GitHub's macos-15 runner; macos-14, which
    built them until 1.0.0-rc.2, is being retired by GitHub. The pages and the Homebrew formula name
    the release candidate as the version to use.
  • A release candidate is published like a release: the image tag latest follows every tag, and
    the apt and dnf repositories have one suite, stable, with every version, a candidate as
    1.2.3~rc.1, which sorts before 1.2.3. The suite testing and rpm/esj-testing.repo are
    gone (a source line or .repo file added for them is to be removed); the next run of
    dist/repositories.sh moves the rpm packages of testing into stable. release.yml started
    with publish_version sets latest and regenerates the repositories for a version already
    released, and builds nothing (docs/releasing.md).

Fixed

  • The native executables — the release archives, the Homebrew formula, the .deb and .rpm
    packages — ended with exit code 5 and internal error: java.util.MissingResourceException: Could not load any resource bundle by com.sun.org.apache.xerces.internal.impl.msg.XMLMessages
    on every XML document that is not well-formed — cut short, with a wrong end tag, an undeclared
    entity, a control character or markup after the root element — in every command that reads
    one, and on a hybrid PDF whose attachment or XMP packet is not well-formed; and in
    esj validate on every document its schema refuses (…XMLSchemaMessages). The jar answers
    exit code 2, the document is not well-formed XML that this reader accepts, or reports the
    finding with exit code 1, and so do they now. The parser of the Java runtime formats each error
    from a resource bundle even where the tool prints a sentence of its own, and no case of
    dist/cases.sh had fed it such a document, so the GraalVM agent never recorded the two
    bundles: every native executable released up to 1.0.0-rc.2 lacks them. The jar, the runtime
    image and the container image were not affected. The cases input-* now take every kind of
    input a reader refuses or reads the hard way, from dist/inputs/,
    through validate, inspect, convert and render --html, so the agent records what they
    reach and the smoke test compares every packaged artefact's answers with the jar's.
  • The native executables refused an invoice in Windows-1252, ISO-8859-15, Shift_JIS or any other
    encoding beyond UTF-8, UTF-16, ISO-8859-1 and US-ASCII with exit code 2 (the document is written in, or declares, the encoding …, which this reader does not read), and the repair of
    a document declared UTF-8 whose bytes are Windows-1252, both of which the jar reads: an image
    carries only those charsets unless it is built with -H:+AddAllCharsets, which it now is. The
    runtime image refused the encodings of the module jdk.charsets, Shift_JIS among them, and
    now carries it. Cases of dist/inputs/ in each of these encodings keep both answering as the
    jar does.
  • The Linux packages of a pre-release are attached to the GitHub release with a dot in place of
    the ~ of their version (esj_1.2.3.rc.1_amd64.deb), which is the name GitHub stores them
    under anyway, and their checksum files name them so: those of 1.0.0-rc.2 name
    esj_1.0.0~rc.2_amd64.deb, which a download does not have, so sha256sum -c fails on them
    (compare the digest by hand). The packages in the apt and dnf repositories keep the ~, and a
    release, which has none, is attached as before. The release job also publishes a pre-release
    only when GitHub lists every asset under the name it was delivered with
    (docs/install.md).

EN 16931 Semantic JSON 1.0.0-rc.2

Pre-release

Choose a tag to compare

@github-actions github-actions released this 16 Oct 19:17
Immutable release. Only release title and notes can be modified.

Release candidate of 1.0.0. The format is 1.0 (SPEC.md 1.0, release candidate); a reader of this version reads documents of 1.0 and 0.1, and a 0.1 document keeps its version, its canonical bytes and both digests. Channels: Maven Central de.bsnsoft.esj:*:1.0.0-rc.2, the archives and Linux packages below, ghcr.io/bsnsoft/esj:1.0.0-rc.2 (latest stays 0.9.8), the apt and dnf suite testing at https://apt.bsnsoft.de as 1.0.0~rc.2; the Homebrew tap stays on 0.9.8. The tag v1.0.0-rc.1 carries the same software: its release was published without assets and is deleted, and it is on no other channel.

Fixed

  • release.yml creates a pre-release as a draft, attaches the assets and publishes it then:
    releases are immutable once published, so assets can be attached only before publication, and
    the release action publishes a pre-release before it uploads them. The tag v1.0.0-rc.1 was
    published that way, got no assets, and is on no other channel; its release is deleted, and
    1.0.0-rc.2 is the same software with this fix (docs/releasing.md).

Changes since 0.9.8 (the section of 1.0.0-rc.1 in the changelog)

Format

  • The format is 1.0, and the specification is SPEC.md version 1.0, a release candidate: a
    document written now carries "version": "1.0". Nothing else of the format changed — the path
    grammar, the value model, the canonical form, the digests, the finding codes, the registries
    and the limits are those of 0.1 (SPEC.md appendix D).
  • A reader of 1.0 reads documents of version 1.0 and of 0.1, the release candidate, both by
    the rules of 1.0, and refuses every other value with ESJ-L1-ENVELOPE-VALUE, whose message
    names the two; in general a reader of 1.x reads 1.y for y ≤ x and 0.1 (section 4.3).
  • A document keeps the version it was read in. The canonical and the pretty form write it, so a
    stored 0.1 document keeps its canonical bytes and both digests, and a document derived from one
    — edited, given derived totals, written up to another edition and back — stays 0.1. A document
    built from nothing, by the domain API or the typed builder, or imported from UBL, CII or PDF
    carries 1.0. The same values as 0.1 and as 1.0 share the semantic digest and differ in the
    document digest (sections 7.3 and 8.3); no command rewrites a 0.1 document as 1.0.
  • schema/esj.schema.json takes version from ["1.0", "0.1"], and the schema identifiers move
    from tag:bsnsoft.de,2026:esj/schema/0.1/… to …/schema/1.0/…; the generated schemata of the
    two editions follow.
  • The fixture manifest records documents of version 0.1 with the canonical bytes they had in a
    new section earlierVersions (conformance/fixtures/version-0.1/), a case of the canonical
    order whose bytes carry 0.1, and rejected documents with the versions 2.0, 1 and 1.1.
    Every other document of the repository — examples, corpus, fixtures, appendix B — is written in
    1.0, and its document digest moved with it; no semantic digest did.

Added

  • Esj.acceptedFormatVersions(), SemanticDocument.formatVersion() and
    SemanticDocument.Builder.formatVersion(String), which accepts only a version this
    implementation reads. Esj.formatVersion() answers 1.0. Two documents that differ in their
    format version alone are not equals, as their document digests differ.

Changed

  • The validation report says under its verdict, in German and in English, that the verdict is
    technical conformance with EN 16931 and that the German VAT classification is a separate question
    (BMF letter of 15 October 2026, Rn. 22 f.). The documentation is checked against that letter, the
    third on mandatory e-invoicing in Germany; nothing in the format, the registries, the bindings or
    the rule packs had to change. docs/legal-de.md maps it paragraph by paragraph
    to what ESJ offers, docs/validation.md separates the verdict from the VAT
    view, docs/b2c.md covers prices agreed gross between businesses as well, and both
    FAQ pages answer for a final invoice after partial invoices, construction work with a bill of
    quantities and a correction.

EN 16931 Semantic JSON 0.9.8

Choose a tag to compare

@github-actions github-actions released this 14 Oct 00:58
Immutable release. Only release title and notes can be modified.

Added

  • Linux packages and package repositories: the native executable as esj_<version>_<arch>.deb
    and esj-<version>-1.<arch>.rpm for x86_64 and arm64, built with nfpm (dist/packages.sh,
    nfpm 2.47.0 pinned by SHA-256 in dist/nfpm.sh), attached to the release with their checksums
    and build provenance attestations, and published in an apt repository and a dnf repository at
    https://apt.bsnsoft.de, served by GitHub Pages from the branch gh-pages:
    apt-get install esj and dnf install esj after adding a keyring and a source line or a
    .repo file (docs/install.md). The executable is
    installed under /usr/lib/esj beside its shared libraries, with /usr/bin/esj a link to it;
    the packages require glibc 2.34 and zlib and nothing else, measured on debian:12-slim,
    ubuntu:22.04, ubuntu:24.04, fedora:latest and rockylinux:9 (dist/packages-smoke.sh).
    A release goes to the suite stable, a pre-release to testing as 1.2.3~rc.1, which sorts
    before 1.2.3. The indices are signed with the release key, the key the artefacts on Maven
    Central are signed with, inline and detached (InRelease, Release.gpg, repomd.xml.asc),
    and so is every rpm; apt reads the indices by hash. dist/repositories.sh regenerates both
    repositories from the packages on the branch, which is their whole state; the job Package
    repositories
    of release.yml runs it on a tag, and dist/repositories-test.sh puts both
    through apt and dnf end to end with a throwaway key and the commands of the install page, in
    the CI and before a release attaches anything.
  • dist/checks.sh fails a native Linux executable, or a library beside it, that needs a glibc
    symbol version newer than ESJ_GLIBC_FLOOR (2.34), the floor the packages require; the native
    build and dist/packages.sh run the check.

Fixed

  • esj serve kept the connection of an early refusal alive — an answer sent before the request
    body was read to its end — though it closes that connection where the lingering read of the
    rest ends at its time bound or at the bound of the door; a client that had already sent its
    next request on it lost that answer (HTTP/1.1 header parser received no bytes: once in 420
    requests on a CI runner, in a test run some 3.5 s longer than usual, past the test's 3 s
    bounds; every time behind a chunked body past the bound). Such an answer now says
    Connection: close, so the next request goes on a new connection; a request whose body was
    read to its end keeps its connection.
  • The native executables ended esj render --template with exit 5 on a JPEG letterhead or logo
    (NoSuchMethodError: …JPEGImageReader.readInputData) and on every PNG that is not plain RGB or
    a palette — with an ICC profile or an sRGB chunk, in grey, interlaced or with an alpha channel
    (sun/java2d/Disposer, CMMException): no case had used such an image, so the native image
    lacked the JNI and resource metadata of the JPEG decoder, of the rasters and colour models of
    ImageIO and of the colour management of the Java runtime. Four template cases over
    dist/artwork/ record it, and the smoke compares their renderings with the jar's.

EN 16931 Semantic JSON 0.9.7

Choose a tag to compare

@github-actions github-actions released this 10 Oct 19:10
Immutable release. Only release title and notes can be modified.

Added

  • esj serve (preview): the tools of the command line over HTTP — a REST API under /api, its
    OpenAPI 3.1 description at /openapi.json, and an MCP server over Streamable HTTP at /mcp —
    and esj mcp (preview), the same tools as an MCP server over the standard streams. Seven tools
    from one table: validate, summary, get, convert, render, extract and inspect, and
    upload over HTTP. The server reads no document itself: every call runs esj as a child process
    with --job-heap, --job-timeout and an environment of its own, at most --max-jobs at once.
    POST /api/validate answers with the report of esj validate --output json, byte for byte.
    Loopback by default and 0.0.0.0 in both container images (ESJ_SERVE_BIND, port 8080
    exposed), a bearer token from --token-file, Origin checked, path only below
    --allow-dir, uploads and artefacts kept for --ttl in one temporary directory that SIGTERM
    removes. The server holds no document and no answer whole: it reads what a child wrote from
    its file, keeps what a result carries and writes the answer into a spool before the call gives
    back its child's place, so a call costs its heap at most 6 MiB whatever --limits lets a
    child read and a client that reads slowly costs none of it, and it starts only with the
    --max-jobs its heap holds (ESJ_SERVE_HEAP in the container image, beside ESJ_MAX_HEAP
    for the children); an MCP batch carries at most 16 messages and one tools/call. Links are
    built from --public-url or a Host the server answers to (--allow-host) and are paths
    otherwise; a file with a second name below --allow-dir is refused; the texts a model reads
    escape every invisible character; notifications/cancelled ends the child of its call.
    Everything the server holds on disk — request bodies and uploads as they are read, artefacts,
    answers that wait for their client, and 64 MiB for each running child from its start, the
    most it may write — is held to --max-disk (448M, within the tmpfs of dist/compose.yaml;
    --max-stored-bytes 256M within it): a call that does not fit is refused with 503 and
    Retry-After, never cut, and a call whose child the server ends as it stops is told so with
    503, not 502. A request refused before its body was read is answered first, and the rest of
    its body is read and dropped, for at most 5 s, before the connection is closed, so that the
    client reads the answer and not a TCP reset. dist/compose.yaml runs it as the service
    esj-api; dist/serve-smoke.sh holds every packaged artefact's server against the jar's.
    docs/serve.md.

Fixed

  • The image stage of dist/Dockerfile.native carries the runtime libraries beside the executable,
    without which a rendering in it failed to load the AWT library.

EN 16931 Semantic JSON 0.9.6

Choose a tag to compare

@github-actions github-actions released this 08 Oct 20:22
Immutable release. Only release title and notes can be modified.

The Java implementation and the TypeScript and C# bindings now read, report and load registries
alike. An entry without a prefix holds for all three; a prefix names the implementation it is
about.

Added

  • The fixture manifest records the whole answer of a validation (contract version 2): the status,
    the layers not evaluated with their reasons, and every finding with its path, code, subject —
    the empty one included — and severity, information findings such as ESJ-L2-NOT-CHECKED and
    ESJ-L2-EDITION-UNKNOWN among them. run.py compares the findings of the reader and those of
    one path at layer L2 in order, as SPEC.md sections 9.6 and 9.2 fix it, and the rest as a set,
    and a rejected document by its whole list of findings rather than its first.
  • Fixture manifest: a section bounds reads documents under bounds below the defaults, each at a
    bound and one past it; a section registryChecks asks a loader to read and combine registry
    files and records whether it takes them; the grammar tables cover the edition grammar of
    semanticModel and the owner token, and more decimal, date, time and base64 candidates; a
    rejected document the reader reads carries its digests; a canonical order case carries its
    document digest and its number of values. A validate request may name the registries to
    validate with (registries), which the fixture bindings of TypeScript and C# answer.
  • Fixtures: 13 documents in examples/invalid/ (the encoding, the JSON text, a missing envelope
    member, a wrong version, the edition grammar, an undefined member of source, a duplicate in
    the envelope and below an owner token, a parent chain, a missing index, a missing group) and,
    under conformance/fixtures/, 64 variants layer L1 refuses, 10 documents whose findings at
    layers L2 and L3 are pinned as a list, 4 documents a reader reads for what a validator says of
    them, 69 cases under bounds, 30 sets of registries, 4 documents not in canonical form, and the
    two documents of SPEC.md appendix B with their canonical bytes. A test holds the bytes,
    lengths and digests the appendix prints to those files and to the manifest.

Changed

  • Java: EsjReader reads the bytes with a scanner of its own instead of jackson-core: it reads
    UTF-8 and nothing else, and it judges every member name before the value written under it. It is
    faster than before and holds less while it reads.
  • The subject of a reader finding is a member access with one grammar: a name the
    specification defines at that place is dotted (source.syntax, values["/BT-1"].scheme),
    every name the document chose is written in brackets (["profile"], source["origin"],
    values["/BT-1"]["note"], extensions["a.b"]["x"][0]), and the name is never cut to an
    excerpt. The bindings wrote such a name dotted (profile, source.foo, values["/BT-1"].foo).
  • A duplicate member name is named by its own member access (values["/BT-1"], format,
    source.syntax, extensions["a.b"]["x"]), not by the object it occurs in; inside a value
    object, a repeated name or a name with a lone surrogate names the value (values["/BT-1"]).
  • Every missing required envelope member is a finding of its own, with the member's name as its
    subject, in the order format, version, semanticModel, values (was the first alone).
  • Every member name is held to maxStringBytes, counted in UTF-8 bytes (the bindings counted
    UTF-16 code units), also below a value the reader walks past, and so are format, version,
    semanticModel and source.sha256 as source.syntax was: past the bound each is
    ESJ-L1-LIMIT rather than the code of its grammar (ESJ-L1-OWNER-TOKEN, ESJ-L1-PATH-SYNTAX,
    ESJ-L1-ENVELOPE-MEMBER), which still applies inside the bound. A bound shorter than the
    envelope's own strings refuses every document; the edition string of 2017 is 30 bytes long.
  • A number token longer than maxStringBytes is ESJ-L1-LIMIT wherever it stands, measured
    before it is built; in values it was ESJ-L1-JSON-TYPE when written with a fraction.
  • ESJ-L1-LIMIT carries the path of the member of values it was met in and, as its subject,
    the member whose name or value reached the bound — values["/BT-1"], values["/BT-1"].value,
    extensions["o"][1]; for a member name past the string bound, the object the name stands in —
    also for a bound the parser used to apply, which named nothing. Inside a
    structure the reader walks past, that is the member walked past; names, numbers and the depth
    are bounded there, strings are not.
  • A message that places a defect in the byte sequence names the byte offset at which the token
    begins, counted from zero, rather than where the parser stopped (Java) or a UTF-16 index (the
    bindings).
  • ESJ-L1-JSON is a finding about the document: its path and subject are empty, its message
    names the byte offset. A token where a value belongs that is no complete JSON value — tru,
    truex, 01, 1. — is ESJ-L1-JSON, before anything is said about its type, also where the
    envelope expects another type (the bindings said ESJ-L1-ENVELOPE-VALUE there).
  • An exception carries the path and the subject of the finding it stands for beside its code:
    path() and subject() of EsjFormatException and EsjLimitException in Java; Path and
    Subject in C#, where EsjLimitException.Code is now an instance property and
    EsjLimitException.LimitCode the constant; subject of EsjError in TypeScript, whose path
    is the empty string where the finding names none (was undefined).
  • esj validate --output json writes subject as a string in every finding, "" where it
    names nothing beyond the path, and never null.
  • Layer L2 checks every path independently (SPEC.md 9.2): a core identifier the registry does not
    contain is ESJ-L2-UNKNOWN-TERM once per segment, also beside an extension segment no loaded
    registry defines, which made the whole path ESJ-L2-NOT-CHECKED and the result INDETERMINATE
    before; the segments above an undefined one are held to the index rule; an unknown identifier of
    a loaded extension namespace stays ESJ-L2-NOT-CHECKED, and its message no longer says that the
    registry is not loaded. Of a path whose terms are all known, the index rule, the chain, the
    content and the components are checked and every failure is reported (TypeScript reported the
    first alone), and the findings of a path stand in the order of its segments.
  • The findings of layer L2 name in subject what the path cannot tell apart: the identifier of the
    segment for ESJ-L2-UNKNOWN-TERM, -INDEX-REQUIRED and -INDEX-FORBIDDEN, the term whose group
    chain is wrong for ESJ-L2-PARENT-CHAIN, the component for ESJ-L2-COMPONENT-NOT-ALLOWED and
    -COMPONENT-MISSING. The subject was empty in Java and C#.
  • Java and C#: ValidationResult.merge (Merge) keeps NOT-REQUESTED for a layer the caller did
    not ask for: a LIMIT or PRECEDING-LAYER-FAILED the other result names for it no longer
    displaces it (SPEC.md 9.5). EDITION-UNKNOWN of a validator that was asked for the layer is
    still kept over the NOT-REQUESTED of a clean read, which Merge of C# answered with
    NOT-REQUESTED; both now compose every pair of reasons by one table. C#:
    Validator.Validate(byte[]) composes the read and the structural run with Merge, so an unknown
    edition reads EDITION-UNKNOWN.
  • Java: Registry.load refuses a registry that names terms it does not define — as a parent, in a
    chain or in reusesTerms — and imports nothing, and reports a cardinality below zero or a path
    that does not end at its term as EsjFormatException rather than IllegalArgumentException.
    Registry.withExtension refuses such an extension where its imports do not name this model
    with this edition, which it combined silently before; its refusal of a redefined term no longer
    calls a term of an earlier extension a core term. Registry.admits(extension) asks the edition
    question before combining.
  • TypeScript: registryOf and new Structure refuse a registry the way section 10 does —
    RegistryError for components no value can satisfy, an identifier listed twice or defined by
    core and extension (was: the later one won), and an extension combined with an edition it does
    not import, or naming core terms with no imports at all.
  • esj validate --extension for a document of an edition the extension does not import says so:
    the ESJ-L2-NOT-CHECKED finding names the option, the edition the registry imports and that it
    is not this one, and the cause is the new extension-for-another-edition rather than
    extension-registry-missing.
  • An extension registry defines identifiers of its own namespace only: one that defines an
    identifier without a namespace — BT-999 as well as BT-1 — or identifiers of two namespaces
    is refused when it is read, where it imports a core, and where it is combined with one.
    model/registry.schema.json states the same for a registry that carries imports.
  • C#: Registry.Load refuses a registry that names an identifier it does not define and imports
    nothing, and WithExtension asks for the imports wherever an extension names an identifier it
    does not define, as Java and TypeScript do, not only one of the core.
  • TypeScript and C#: a member name is judged before the colon after it, as in Java: {"a" 1} is
    ESJ-L1-ENVELOPE-MEMBER rather than ESJ-L1-JSON, and a name of values that is no path draws
    ESJ-L1-PATH-SYNTAX before the missing colon ends the read.
  • A string past its bound is read on to its end for a lone surrogate, which comes first:
    TypeScript and C# refused a string of the envelope, of source or of extensions past the
    larger of the two string bounds, and a string of values past twice the string bound, for its
    length alone. Strings of the envelope, of source, of extensions and of values are he...
Read more

EN 16931 Semantic JSON 0.9.5

Choose a tag to compare

@github-actions github-actions released this 07 Oct 22:49
Immutable release. Only release title and notes can be modified.

Added

  • Six negative fixtures under examples/invalid/, run by the fixture manifest over the Java, the
    TypeScript and the C# reader: extension-number-exponent-overflow and -underflow
    (ESJ-L1-EXT-NUMBER), value-depth-32 and value-depth-33 (ESJ-L1-JSON-TYPE, then
    ESJ-L1-LIMIT: the walk past a structure inside values), path-syntax-with-array-value
    (ESJ-L1-PATH-SYNTAX and ESJ-L1-JSON-TYPE) and values-deep-array (ESJ-L1-ENVELOPE-VALUE).
  • de.bsnsoft.esj.Preview, an annotation for what is published to be used and judged and may
    change in any minor release. It marks the packages …typed.v2026, …rules.en16931.v2026,
    …upgrade and …b2c, the types MinorUnits, PackFetcher, PackRecipe, PackRecipes,
    ContainerChecks, InvoiceAttachments and En16931V2026Pack, and the method
    RulePackSource.currencyMinorUnits(). The generator writes the package comment of the 2026
    view with it.
  • Registry.isPreview(): whether a registry describes an edition this project ships as a preview.
  • de.bsnsoft.esj.imports.InvoiceReader, ImportResult read(byte[] xml): StreamingReader and
    XrImporter implement it, and PdfInvoiceImporter.importPdf hands the attachment to one.
  • The package de.bsnsoft.esj.xml in esj-core, the front door of an XML invoice:
    InvoiceSyntax with of(namespace, localName), EncodingMode, XmlEncodingReport with
    of(byte[]), and XmlEncodingException. Both readers, the syntax engine and the PDF container
    name a syntax and an encoding with these types.
  • Registry.en16931WithXrechnung(): the core model with the XRechnung extension, built once; the
    readers, the renderers and XrExporter use it by default.
  • EsjLimitException.bound() names the bound that was reached as an EsjLimitException.Bound:
    the setting (maxInputBytes, maxPages, maxRuntime, …), its value and its unit. Every bound
    of every module names it.
  • RenderEngineException, the failure of the rendering machinery, which RenderException stood
    for itself before.
  • CiiWriter.semanticModel(), CiiWriter.supports(String), UblWriter.semanticModel() and
    UblWriter.supports(String).
  • Every jar names its module: Automatic-Module-Name is de.bsnsoft.esj. followed by the
    artifact identifier without esj- (de.bsnsoft.esj.core, …); MavenCoordinatesTest holds every
    published jar to it.
  • The Maven profile api-check compares the API of every library module with the release
    esj.api-baseline names, with japicmp, leaving out internal packages, the preview packages and
    everything marked @Preview. It reports and does not fail before 1.0.0; the CI job API compared
    with the last release
    keeps the report (docs/releasing.md). Against 0.9.4 it names 124 classes
    with incompatible changes, all of them the moves, renames and removals of the migration table and
    the preview marks.
  • The rule files of both bundled packs, as their manifests name them, are validated against
    rules/rule.schema.json on every build; the 2026 pack's test is part of what
    bin/without-edition-2026.sh removes.
  • Three negative fixtures carry, in a member name, every class of character SPEC.md section 9.5
    escapes: path-syntax-terminal-characters (ESJ-L1-PATH-SYNTAX),
    owner-token-terminal-characters (ESJ-L1-OWNER-TOKEN) and
    envelope-member-terminal-characters (ESJ-L1-ENVELOPE-MEMBER). The fixture manifest records
    the subject of a finding where section 9.5 requires one — where the path is empty, and on a
    finding of layer L3 — and run.py and the manifest tests of both bindings compare it there; a
    binding's validate answer carries subject beside path and code.
  • docs/compatibility.md: what a release may change from 1.0.0 — the format, the command line,
    the Java libraries, the data they ship — and the rules the Java API keeps; docs/java-api.md
    lists every public package as API, preview or internal.
  • Registry.editionKey(), Registry.defaultEditionKey() and Registry.editionKeys(): an edition
    is named three ways and each has its method — the key (2017), edition() (the title) and
    semanticModel() (the spelling of a document).
  • de.bsnsoft.esj.render.PaymentCode (TEMPLATE, DRAW, OMIT), what RenderOptions says
    about the EPC QR code of the letter.

Changed

  • esj render --html is held to --max-output-bytes (64 MiB, 1 GiB under --limits large), as
    every XML document the tool writes is: the page is measured in bytes of UTF-8 while it is
    written, and one past the bound leaves with exit code 7 and nothing written, rather than being
    finished in memory first. In the library the bound is RenderOptions.maxHtmlBytes()
    (withMaxHtmlBytes(long), 1 GiB by default) and is reached with an EsjLimitException.
  • esj-render replaces U+007F and the C1 controls with a space in every form — the HTML
    rendering and the report carried them as they stood, the PDF printed a question mark — and
    turns U+0085, U+2028 and U+2029 into line feeds in all of them; one class holds the sets for
    every form.
  • dist/package.sh smoke compares a container image only when dist/package.sh docker built it
    from the jar it is compared with, and fails when the image that target built is gone or stale;
    an older image of the same tag is named and not compared. dist/smoke.sh takes a relative path
    in a command relative to the directory it was run from.
  • bin/without-edition-2026.sh makes its copy under $TMPDIR and removes it when it ends;
    --keep keeps it.
  • -Dsurefire.failIfNoSpecifiedTests=false lets mvn -Dtest=<class> -pl <module> -am pass the
    modules without that test; without it, a pattern that matches no test still fails.
  • esj-render's sRGB profile is recorded as compared byte for byte with the file the
    International Color Consortium publishes (icc/README.md, docs/sources.md).
  • The bindings carry their publisher's names and the project's version: the npm package is
    @bsnsoft/esj (was en16931-semantic-json), the C# package, assembly and namespace
    BSNSoft.Esj (was En16931.SemanticJson, projects under bindings/csharp/BSNSoft.Esj*). The C#
    build reads the version from pom.xml; npm run sync-version writes it into package.json and
    package-lock.json, and a test fails while they differ. Nothing is published yet.
  • EN 16931-1:2026 is a preview, and says so: esj --version prints
    semantic model registries 2017, 2026 (preview), esj inspect and esj validate print
    Semantic model: EN16931-1:2026 (preview), the validation report writes (preview) or
    (Vorschau) beside the edition, esj upgrade writes an info: line where the edition it reads
    or writes is one, and esj --list-packs marks the pack en16931-2026/0.1. The JSON output is
    unchanged. README.md, SPEC.md (section 1; the format stays 0.1), docs/editions.md,
    docs/java-api.md and the FAQ say so too.
  • The Javadoc leaves out every package whose name contains internal.
  • esj-syntax and esj-render declare Saxon-HE, which they call directly; esj-cli takes
    esj-typed in test scope only, so the self-contained jar no longer carries the typed view it
    never loads.
  • One set of characters that steer a terminal, Esj.steersATerminal(int) and
    Esj.isBidiControl(int) in esj-core, is used by every text output: the messages of
    esj-core, esj-rules and esj-pdf, the lines of esj list, esj inspect, esj diff,
    esj extract --list and of every report, and the error stream. The JSON reports write the same
    characters as JSON escapes. Until now list, inspect and diff wrote C1 controls (CSI,
    NEXT LINE), U+061C, U+2028 and U+2029 as they stood, and a message of a registry quoted its
    content unescaped. esj get still writes a value raw. esj-render takes the bidirectional
    controls from there as well and adds the interlinear annotation characters U+FFF9–U+FFFB,
    which only a rendering replaces; the messages of esj-rules and esj-pdf write \u with
    lowercase digits, as esj-core does.
  • Every command that reads a document holds the five-minute deadline where --max-runtime names
    none: convert, upgrade, inspect, extract, get, list, diff, canonicalize, and
    embed without --verapdf, as validate and render did. Exit code 7 and no verdict. Over
    conformance/scale (80 MB of UBL and CII, rich and dense lines, --limits large) each of them
    ends in 2 to 22 s; --importer xslt takes 2 minutes over 25 MB and needs a larger
    --max-runtime from about 40 MB.
  • examples/smallest-valid.esj.json states BT-110 (0): it is VALID through both syntaxes, where
    --via ubl said INDETERMINATE (exit code 9) because UBL requires the tax amount beside a VAT
    breakdown. It has 28 terms, and removing any of them ends the verdict through one of the two.
  • XmlEncodingReport has decodes() and XmlEncodingException has repairable(); the earlier
    constructors stay.
  • One exception hierarchy: EsjException is the abstract root of every exception the libraries
    throw, no longer sealed. XrException, BindingException, SyntaxException, PdfException,
    RenderException, PackException, RulePackException, DerivationException,
    MissingValueException, ValueTypeException, BuildException and
    PolicyPreconditionException extend it. RenderException is abstract and sealed, its kinds
    final: RenderEngineException, TemplateException, RenderContentException.
  • Every bound of every module raises EsjLimitException, which is no longer final and no longer
    belongs to a module's hierarchy: catch (PdfException e) or catch (BindingException e) no
    longer catches a limit. The messages are the ones of 0.9.4.
  • PdfInvoiceImporter.importPdf(byte[]) reads the attachment with the StreamingReader, as the
    command line does; it was the importer of esj-xr. The document is the one esj convert writes
    for t...
Read more

EN 16931 Semantic JSON 0.9.4

Choose a tag to compare

@github-actions github-actions released this 07 Oct 08:15
Immutable release. Only release title and notes can be modified.

Added

  • Pack directories: esj validate and esj inspect take --packs <directory> (repeatable) and
    read the environment variable ESJ_PACKS (a path list); every <id>/<version>/<release>/pack.json
    found there joins the bundled packs and is chosen by the profile of the document. A pack with the
    identity of a bundled one, and a profile two packs recognize, are refused by name — except where
    those packs are releases of one pack (same id and version): then the newest release is taken,
    compared part by part (3.0.10 after 3.0.9), and --pack takes another. Every report
    names the origin of the pack beside its identity: syntax.pack.source is directory and the new
    syntax.pack.location the directory it was read from (null for a bundled pack), and
    esj --list-packs prints origin for every pack.
  • esj packs list and esj packs fetch <recipe> --into <directory> [--replace]. fetch follows a
    recipe this build carries for artefacts that may be used but not redistributed: it downloads the
    files over https, refuses any whose SHA-256 is not the one the recipe pins, compiles the
    Schematron to XSLT in process with the ISO Schematron XSLT 2 skeleton (MIT, now in
    esj-syntax), copies the schema modules out of the bundled pack and writes a pack whose
    manifest records the source URLs, the fetch date and the SHA-256 of every file. It is the only
    command that opens a network connection.
  • The recipes peppol-bis-billing-3.0.21 and peppol-bis-billing-3.0.20: the Peppol BIS
    Billing 3.0 validation artefacts, as published by OpenPeppol — the Peppol rules for UBL and CII
    and the EN 16931 Schematron that release ships (1.3.16 and 1.3.15). 3.0.21 is the May 2026
    release, mandatory from 2026-08-17; OpenPeppol tagged no 3.0.21, so its recipe fetches below
    the commit 806866bd2bd91d7e9623b68f08164e8fbe9e67a0, never by a branch name, and a recipe
    without a tag may fetch only below the commit it names. esj packs fetch peppol-bis-billing
    follows the newest recipe this build carries (3.0.21); esj packs list marks it. None of those
    files is in this repository or in any artefact; esj validate on a Peppol invoice without the
    pack names the command that makes it. With 3.0.21, OpenPeppol's 10 examples validate VALID
    and all 62 rules its own unit tests cover fire as those tests expect; with 3.0.20, 9 examples
    and 58 rules (conformance/peppol/README.md, run with -Desj.network=true and in the JDK 21
    job of the continuous integration).

Changed

  • A recipe's source.tag is optional, with a new optional source.branch; the manifest of a
    fetched pack records them as the recipe has them, and esj packs list names the revision a
    recipe fetches by (at tag v3.0.20, at commit … (branch 2026-Q2-QA2)).

  • A PDF written by 0.9.0 to 0.9.3 still carries invoice.esj.json. It is read as an attachment
    that is not the invoice, as a logo would be: esj inspect and esj extract --list list it
    (not XML, application/json, Supplement), esj extract --attachment invoice.esj.json
    hands out its bytes, and esj validate reports nothing about it. A PDF whose one attachment it
    is carries no invoice (exit code 2).

  • docs/java-api.md names Saxon-HE 13.0 as the minimum: with 12.x the XML Schema, Schematron
    and XSLT path still run, and every HTML page of esj-render, the HTML report included, fails
    with AbstractMethodError (UnparsedTextURIResolver). docs/install.md notes that the jar
    carries Saxon-HE 13.0 unrelocated.

Removed

  • The ESJ document inside the hybrid PDF. esj embed and esj render --embed cii write one
    embedded file, the invoice XML. Gone with the second file invoice.esj.json: the rule that it
    and the XML are two accounts of one invoice, the option --no-esj and the line saying the
    attachment went in, the container row ESJ document attached of esj validate (lines, the
    JSON member container.esj, the HTML and the PDF report), the findings PDF-ESJ-DISAGREES,
    PDF-ESJ-UNSOUND, PDF-ESJ-UNREADABLE, PDF-ESJ-UNCHECKED, PDF-EMBEDDED-SEVERAL-ESJ and
    PDF-EMBEDDED-ESJ-LABEL, and section 15 of SPEC.md (informative). A receiver of two
    representations of one invoice has to parse and compare both; the XML is the invoice, and the
    ESJ document derives from it at any time (docs/design-decisions.md).
  • The API of that feature, which breaks callers of it: in esj-pdf the classes EsjAgreement
    and EsjAttachment, EmbedOptions.withEsj and the esj component of EmbedOptions (its
    constructors take one argument fewer), EmbedResult.EsjOutcome, EmbedResult.esj(),
    esjAttached() and esjOmitted() (its constructor takes the file and the report),
    AttachmentKind.ESJ_DOCUMENT, ContainerFinding.Category.PDF_ESJ and the three-argument
    ContainerChecks.run; in esj-core the phrases Phrase.ROW_PDF_ESJ, ESJ_ATTACHMENT and
    ESJ_ATTACHMENT_PARTLY.

Fixed

  • UBL Credit Notes keep the sub lines of the XRechnung extension. The extension's source binds
    them for UBL Invoice only, so the streaming reader dropped every cac:SubCreditNoteLine
    without an observation, and the writer had no place for BG-DEX-01. ubl-creditnote.json now
    binds BG-DEX-01 to BG-DEX-08 and the 36 core terms a sub line reuses below
    cac:CreditNoteLine//cac:SubCreditNoteLine, quantity from cbc:CreditedQuantity (two
    corrections, model/bindings/README.md). BG-DEX-09 and BT-DEX-001 to BT-DEX-003 stay unbound:
    UBL 2.1 gives the credit note no cac:PrepaidPayment. Measured on
    conformance/creditnote/04.01a-CREDITNOTE_ubl.xml, the KoSIT extension instance 04.01a as a
    credit note: read, written, read again and accepted by the pack, with the values of the
    invoice. The XSLT path (--importer xslt) still gives every sub credit note line the values
    of the first credit note line, a defect of the vendored KoSIT stylesheet
    (conformance/creditnote/README.md).
  • esj-bom no longer hands on the versions the build of this project manages. The deployed POM
    is flattened: no parent, no properties, and only the modules of this project in
    dependencyManagement. Importing 0.9.0 to 0.9.3 also imported JUnit 6.1.3, Saxon-HE, PDFBox,
    Jackson and the PostgreSQL test binaries, and so decided the JUnit version of the importing
    build.

Full Changelog: v0.9.3...v0.9.4

v0.9.3

Choose a tag to compare

@github-actions github-actions released this 29 Sep 13:11

Full Changelog: v0.9.2...v0.9.3

EN 16931 Semantic JSON 0.9.2

Choose a tag to compare

@github-actions github-actions released this 26 Sep 19:36

Added

  • 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 lin...
Read more

EN 16931 Semantic JSON 0.9.1

Choose a tag to compare

@github-actions github-actions released this 22 Sep 20:40

Fixed

  • The macOS native executable of 0.9.0 could not render: its archive shipped without the shared
    libraries of the Java runtime that a rendering loads (Can't load library: awt). The packaging
    now copies them from the GraalVM that built the image where native-image leaves them out and
    fails the build where any is missing; a smoke test whose cases differ from the jar fails the
    build again instead of being lost in a pipe. The GraalVM build the executables are made with is
    now pinned by release and SHA-256 (dist/graalvm.sh): the 25.0.x line of the community builds,
    which the release runner had picked, produces a macOS image that cannot load libawt even with
    the libraries beside it; the 25.3 line does.