<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://goerch.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://goerch.github.io/" rel="alternate" type="text/html" /><updated>2026-05-27T17:42:50+00:00</updated><id>https://goerch.github.io/feed.xml</id><title type="html">Off the Clock</title><subtitle>Everything the company does not claim</subtitle><entry><title type="html">Replacing SSRS After a SQL Server to PostgreSQL Migration</title><link href="https://goerch.github.io/reporting/open-source/data-engineering/postgresql/2026/05/27/replacing_ssrs_after_a_sql_server_to_postgres_migration.html" rel="alternate" type="text/html" title="Replacing SSRS After a SQL Server to PostgreSQL Migration" /><published>2026-05-27T07:00:00+00:00</published><updated>2026-05-27T07:00:00+00:00</updated><id>https://goerch.github.io/reporting/open-source/data-engineering/postgresql/2026/05/27/replacing_ssrs_after_a_sql_server_to_postgres_migration</id><content type="html" xml:base="https://goerch.github.io/reporting/open-source/data-engineering/postgresql/2026/05/27/replacing_ssrs_after_a_sql_server_to_postgres_migration.html"><![CDATA[<p><em>A practitioner’s notes on leaving SQL Server Reporting Services behind, choosing open-source tooling under budget pressure, and validating the decision unconventionally.</em></p>

<hr />

<h2 id="context">Context</h2>

<p>We inherited a legacy project. The database was SQL Server, the reporting layer was SQL Server Reporting Services (SSRS), and the project budget was comparatively limited. Our task: migrate the database to PostgreSQL and find a workable replacement for a handful of SSRS reports — mostly pivot tables over four data marts, with drilldown capability — serving a small group of internal business users.</p>

<p>This post reflects on the choices we made, the ones we didn’t, and what we learned along the way.</p>

<hr />

<h2 id="the-landscape-why-ssrs-replacement-is-non-trivial">The Landscape: Why SSRS Replacement Is Non-Trivial</h2>

<p>SSRS has always been an acquired taste. It is not particularly friendly to business users, self-service analytics is not its strength, and its architecture is firmly rooted in the Microsoft ecosystem. But it does one thing very well: pixel-perfect, parameterised, paginated reports — including pivot tables with hierarchical drilldown that most modern BI tools struggle to replicate cleanly.</p>

<p>When you leave that ecosystem, you quickly discover that “replace SSRS” is not a single well-defined problem. It depends entirely on what you were actually using it for.</p>

<p>For organisations staying within the Microsoft world, the natural path is <strong>Power BI Report Server (PBIRS)</strong> — the on-premise successor that Microsoft now ships in place of SSRS, which was notably <a href="https://techradar.info/what-is-the-replacement-for-ssrs-modern-alternatives-for-2026/">absent from SQL Server 2025</a>. PBIRS supports the same RDL report format as SSRS, meaning many existing reports can be migrated without a complete rewrite. For cloud-first teams, <strong>Power BI paginated reports</strong> in the Power BI Service is the equivalent cloud-native path.</p>

<p>However, once you have moved to PostgreSQL, Power BI becomes considerably less straightforward. PostgreSQL is <a href="https://community.fabric.microsoft.com/t5/Desktop/Power-BI-Report-Builder-PostgreSQL/td-p/2519632">not a natively supported embedded data source for Power BI paginated reports</a> — only Azure SQL Database and Azure Synapse Analytics are supported out of the box. Connecting to Postgres requires routing through ODBC or a data gateway, and <a href="https://medium.com/@leadvic/postgresql-on-premise-and-powerbi-a-sad-story-that-tells-you-everything-you-need-to-know-before-9c0000bb3d11">that gateway only runs on Windows — not in a container, requiring a dedicated Windows server or VM</a>. For a team that has just moved away from the Microsoft stack, this is a significant and often underdiscussed friction point. Additionally, <a href="https://blog.5000fish.com/ssrs-alternatives">Power BI Pro pricing increased 40% in April 2025</a>, which is relevant context for any long-term cost assessment.</p>

<p>Other commonly mentioned alternatives include <strong>Pentaho</strong> (with a long history in the data warehouse space but a complicated open-source/enterprise split), <strong>Jaspersoft</strong>, and <strong>Bold Reports</strong> (which supports RDL import). For teams building from scratch on open-source tooling, <strong>Apache Superset</strong> and <strong>Metabase</strong> are the most actively maintained options.</p>

<hr />

<h2 id="the-architectural-opportunity-introducing-a-proper-oltpolap-split">The Architectural Opportunity: Introducing a Proper OLTP/OLAP Split</h2>

<p>Our predecessors had not separated operational and analytical concerns. Everything lived together in SQL Server. The migration to PostgreSQL gave us a natural opportunity to fix this — and we took it.</p>

<p>We introduced <strong>dbt</strong> (data build tool) as the transformation layer between the operational database and the reporting marts. This was not a deeply deliberated decision at the time — dbt was an obvious choice given the team’s familiarity — but it proved sound. The four data marts that feed the reports are now explicitly modelled, versioned, and tested as dbt models, rather than being implicit artefacts of ad-hoc queries.</p>

<p>One honest note: <a href="https://sqlmesh.com">SQLMesh</a> has emerged as a compelling alternative to dbt, with stronger support for incremental models and a different philosophy around state management. We would want to evaluate it seriously on future projects, though dbt continues to serve us well here.</p>

<p>The result of this separation is that the reporting layer now queries clean, purpose-built marts rather than the operational tables directly. This is the right architecture regardless of which reporting tool sits on top.</p>

<hr />

<h2 id="finding-a-reporting-tool-the-pivot-table-problem">Finding a Reporting Tool: The Pivot Table Problem</h2>

<p>The central requirement was pivot table support with drilldown. This rules out a large portion of the BI tool landscape, or at least makes it uncomfortable.</p>

<p><strong>Apache Superset</strong> has a Pivot Table V2 that supports row/column grouping and metric aggregation. However, it has real gaps: <a href="https://github.com/apache/superset/discussions/27555">collapsible hierarchical drilldown — the expand/collapse behaviour familiar from Excel and SSRS — was still being requested by users in 2024 with no native solution</a>, and <a href="https://preset.io/blog/superset-repo-update-july-2025/">pivot table Excel export only landed in July 2025</a>. Superset is a powerful and actively developed platform, but its pivot story was clearly less mature at the time of our evaluation.</p>

<p><strong>Metabase</strong> has stronger pivot table support and a more polished user experience for non-technical users. We ran a short evaluation — a day or two — but did not reach a firm conclusion in that time. Metabase remains a serious contender and is our identified upgrade path if requirements grow.</p>

<p><strong>Rill</strong> (<a href="https://www.rilldata.com">rilldata.com</a>) was the other platform we evaluated seriously. It is a younger and more opinionated tool, built around fast operational dashboards and exploratory analytics, with a developer-first workflow that uses YAML and SQL configuration files rather than a GUI. We had prior positive experience using dbt and Rill together on an <a href="https://github.com/idesis-gmbh/GitHubExperiments/blob/master/docs/blog2/README.md">R&amp;D project</a>, which informed our confidence here.</p>

<hr />

<h2 id="how-we-validated-the-decision-two-unconventional-methods">How We Validated the Decision: Two Unconventional Methods</h2>

<p>Faced with cost pressure and no external guidance, we needed a pragmatic way to validate our tooling choice without spending weeks on formal evaluation. We used two complementary approaches.</p>

<p><strong>1. Codebase size as a cognitive load proxy</strong></p>

<p>We used <a href="https://github.com/boyter/scc">scc</a> — a fast, accurate code counter — to compare the size of the Metabase, Superset, and Rill codebases. The reasoning: a smaller, more focused codebase means a shorter path to understanding the tool, debugging unexpected behaviour, and contributing fixes. For a team without a dedicated BI engineer and with a depleted budget, this is arguably more operationally relevant than a feature matrix.</p>

<p>Rill is significantly smaller and more focused than either Metabase or Superset, which are both large, general-purpose platforms.</p>

<p><strong>2. Attempting to fix a real bug</strong></p>

<p>We went further than reading the code — we attempted to <a href="https://github.com/rilldata/rill/issues/9489">fix an actual open Rill issue</a>. This approach tells you things no feature comparison can: How approachable is the codebase? How does the team respond to outside contributors? What is the actual code quality day-to-day?</p>

<p>This kind of due diligence is almost never mentioned in migration write-ups, but it is one of the most honest signals available for evaluating an open-source dependency.</p>

<hr />

<h2 id="what-we-learned-about-rill--including-the-caveats">What We Learned About Rill — Including the Caveats</h2>

<p>Our evaluation surfaced a few things worth noting for anyone considering Rill:</p>

<ul>
  <li><strong>Agentic coding in production</strong>: Parts of the Rill codebase appear to involve AI-assisted development. This is new and relatively uncharted territory for an open-source project, and raises questions about long-term code consistency and maintainability.</li>
  <li><strong>Distributed development</strong>: Development work appears to be partially distributed internationally. Not a disqualifying factor, but relevant context when assessing contributor dynamics and response times.</li>
  <li><strong>No on-premise enterprise offering</strong>: Rill’s hosted, multi-user product is cloud-only. For customers with GDPR constraints, data residency requirements, or simply a reluctance to place business-critical data in a third-party cloud, this is a potential hard blocker.</li>
</ul>

<p>The third point is the most significant for our situation. Our customer would likely be uncomfortable with a cloud-only reporting solution for sensitive business data.</p>

<hr />

<h2 id="our-decision-rill-developer-on-premise">Our Decision: <code class="language-plaintext highlighter-rouge">rill developer</code>, On-Premise</h2>

<p>The resolution is pragmatic: we are deploying <strong><code class="language-plaintext highlighter-rouge">rill developer</code></strong> — Rill’s local, single-user mode — on-premise, without IAM. This sidesteps the cloud dependency entirely for now. The business user gets a working, capable reporting environment. The team gets a tool with a manageable codebase and a developer-friendly workflow.</p>

<p>We have explicitly scoped any future requirement for multi-user access, SSO, or enterprise permissions as a <strong>change request for the customer to fund</strong>. At that point, Metabase becomes the most natural upgrade path — it is a more mature platform for multi-user deployments and has the pivot table capability our customer needs.</p>

<p>This is not a perfect solution. It is a well-reasoned, honest one.</p>

<hr />

<h2 id="summary">Summary</h2>

<table>
  <thead>
    <tr>
      <th>Concern</th>
      <th>Our Approach</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>OLTP/OLAP separation</td>
      <td>Introduced dbt transformation layer</td>
    </tr>
    <tr>
      <td>Reporting tool selection</td>
      <td>Rill developer (on-premise, no IAM)</td>
    </tr>
    <tr>
      <td>Pivot table requirement</td>
      <td>Met by Rill; Metabase as upgrade path</td>
    </tr>
    <tr>
      <td>Cost pressure</td>
      <td>Open-source tooling throughout</td>
    </tr>
    <tr>
      <td>Validation method</td>
      <td>scc codebase comparison + bug fix attempt</td>
    </tr>
    <tr>
      <td>Future migration</td>
      <td>Metabase, scoped as customer CR</td>
    </tr>
  </tbody>
</table>

<p>The broader lesson: “replace SSRS” is not a product decision — it is an architectural decision. The reporting tool is the last piece. Getting the data layer right first (OLTP/OLAP separation, clean marts, dbt) means the reporting tool can be swapped without rebuilding the foundation.</p>

<hr />

<p><em>Prior work using dbt and Rill for R&amp;D analysis is documented <a href="https://github.com/idesis-gmbh/GitHubExperiments/blob/master/docs/blog2/README.md">here</a>.</em></p>]]></content><author><name></name></author><category term="reporting" /><category term="open-source" /><category term="data-engineering" /><category term="postgresql" /><summary type="html"><![CDATA[A practitioner’s notes on leaving SQL Server Reporting Services behind, choosing open-source tooling under budget pressure, and validating the decision unconventionally.]]></summary></entry><entry><title type="html">Measuring and Comparing Open Source E-Invoicing Libraries: A Cautious Look</title><link href="https://goerch.github.io/e-invoicing/open-source/code-quality/standards/2026/04/28/comparing_open_source_e-invoicing_libraries.html" rel="alternate" type="text/html" title="Measuring and Comparing Open Source E-Invoicing Libraries: A Cautious Look" /><published>2026-04-28T05:12:00+00:00</published><updated>2026-04-28T05:12:00+00:00</updated><id>https://goerch.github.io/e-invoicing/open-source/code-quality/standards/2026/04/28/comparing_open_source_e-invoicing_libraries</id><content type="html" xml:base="https://goerch.github.io/e-invoicing/open-source/code-quality/standards/2026/04/28/comparing_open_source_e-invoicing_libraries.html"><![CDATA[<p><em>A developer’s notes on code metrics, ZUGFeRD, XRechnung, and the libraries that implement them.</em></p>

<hr />

<p>I recently spent some time trying to get a sense of two open source e-invoicing libraries —
<a href="https://github.com/ZUGFeRD/mustangproject">mustangproject</a> (Java) and
<a href="https://github.com/stephanstapel/ZUGFeRD-csharp">ZUGFeRD-csharp</a> (.NET) — and came away with
more questions than answers, but also some useful perspective. This post summarises what I found.
I am not a domain expert in e-invoicing standards, so please treat the observations here with
appropriate caution and verify anything that matters to your situation.</p>

<hr />

<h2 id="measuring-code-size-scc-and-the-cocomo-trap">Measuring Code Size: scc and the COCOMO Trap</h2>

<p>A natural starting point is asking “how big is this project?” A useful free tool for this is
<a href="https://github.com/boyter/scc">scc</a> (Sloc, Cloc and Code), a fast Go-based line counter that
also computes cyclomatic complexity and COCOMO cost estimates per language. It is in many ways
the spiritual successor to David A. Wheeler’s <code class="language-plaintext highlighter-rouge">sloccount</code>.</p>

<p>Running <code class="language-plaintext highlighter-rouge">scc</code> on both libraries reveals an immediate problem: the raw totals are dominated by
XML, XSD, and XSLT files, not handwritten application logic. For mustangproject the Extensible
Stylesheet (XSL) category alone accounts for roughly 688,000 lines; for ZUGFeRD-csharp the XML
category runs to around 753,000 lines. Both of these are largely <strong>normative artefacts from
standards bodies</strong> — XSLT stylesheets and Schematron rules from
<a href="https://xeinkauf.de/xrechnung/">KoSIT</a> and <a href="https://www.ferd-net.de/">FeRD</a>, not original code.</p>

<p>When you filter to just the handwritten logic the picture becomes more comparable:</p>

<table>
  <thead>
    <tr>
      <th>Library</th>
      <th>Language</th>
      <th>Code lines</th>
      <th>Cyclomatic complexity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>mustangproject</td>
      <td>Java</td>
      <td>~22,000</td>
      <td>~3,586</td>
    </tr>
    <tr>
      <td>ZUGFeRD-csharp</td>
      <td>C#</td>
      <td>~21,500</td>
      <td>~1,972</td>
    </tr>
  </tbody>
</table>

<p>Two libraries implementing roughly equivalent functionality, at almost identical size — but the
Java is nearly twice as complex per line. Whether that reflects Java’s more verbose OOP patterns
or accumulated conditional handling over a longer project lifetime is hard to say without deeper
analysis.</p>

<h3 id="on-cocomo-estimates">On COCOMO estimates</h3>

<p><code class="language-plaintext highlighter-rouge">scc</code> produces COCOMO “organic” cost and schedule estimates. For mustangproject it produced a
figure in the tens of millions of dollars. This is not meaningful here. The COCOMO model was
calibrated on large 1980s-era institutional projects with significant process overhead. It has no
concept of “this file was transcribed from a normative standards document.” The relative
complexity ratios within COCOMO (more branching → more effort) are still roughly valid as
<em>trends</em>, but the absolute dollar figures should be ignored unless you are specifically modelling
labour costs in a comparable institutional context.</p>

<p>The honest conclusion is that <code class="language-plaintext highlighter-rouge">scc</code> is useful for getting a rough sense of <em>where the mass of a
project lives</em> — how much is handwritten logic versus bundled artefacts, which language carries
the complexity — but effort estimation from static metrics alone is essentially hopeless for
projects of this kind. The moment a codebase bundles normative standards documents, the numbers
stop reflecting developer effort in any meaningful way.</p>

<hr />

<h2 id="the-kosit-test-suite-a-genuine-quality-signal">The KoSIT Test Suite: A Genuine Quality Signal</h2>

<p>A more useful quality indicator than line counts is test coverage against normative reference
cases. For XRechnung, KoSIT maintains an
<a href="https://github.com/itplr-kosit/xrechnung-testsuite">official test suite</a> of real-world business
case invoices and technical edge cases, versioned alongside each XRechnung release. This is a
government-mandated artefact — KoSIT operates on behalf of the
<a href="https://www.it-planungsrat.de/">IT-Planungsrat</a>, the joint federal-state IT standards body.</p>

<p>The ~1,200 XML files found in the ZUGFeRD-csharp repository are almost certainly these KoSIT
test instances, bundled as integration tests. That is a good sign — it means the library is
testing its output against the normative reference. However, it also means those files should
be entirely excluded from any LOC or effort analysis.</p>

<p><strong>For ZUGFeRD there is no equivalent official test suite.</strong> The closest thing is the
<a href="https://github.com/ZUGFeRD/corpus">ZUGFeRD/corpus</a> repository, which is maintained by the
Mustang team rather than FeRD, and is explicitly described in its own README as unofficial and
makeshift. This asymmetry reflects the different governance structures: KoSIT has a legal mandate
and publishes rigorous versioned artefacts; FeRD is an industry forum with no equivalent
obligation.</p>

<hr />

<h2 id="zugferd-and-xrechnung-not-synonyms">ZUGFeRD and XRechnung: Not Synonyms</h2>

<p>It is easy to treat these two terms as interchangeable, but they are architecturally quite
different things built on the same foundation.</p>

<p>Both are implementations of the European standard <strong>EN 16931</strong>, which defines a semantic invoice
data model. On top of that:</p>

<ul>
  <li>
    <p><strong>XRechnung</strong> is a pure XML file — a German CIUS (Core Invoice Usage Specification) that
tightens EN 16931 for public sector use, notably requiring a
<a href="https://www.xeinkauf.de/xrechnung/faq/">Leitweg-ID</a> for routing to the correct government
inbox. It has no visual component. It is governed by
<a href="https://xeinkauf.de/xrechnung/">KoSIT on behalf of the IT-Planungsrat</a>.</p>
  </li>
  <li>
    <p><strong>ZUGFeRD</strong> is a hybrid format: a PDF/A-3 file with an invoice XML embedded as an attachment.
A human can read it in any PDF viewer; the embedded XML can also be extracted and processed
automatically. It was designed primarily for B2B use. It is governed by
<a href="https://www.ferd-net.de/">FeRD (Forum elektronische Rechnung Deutschland)</a>.</p>
  </li>
</ul>

<p>They overlap because ZUGFeRD 2.1.1 introduced a dedicated XRechnung reference profile, meaning
a ZUGFeRD envelope can carry XRechnung-compliant XML. But they remain distinct standards with
distinct governance bodies.</p>

<h3 id="the-legal-timeline">The legal timeline</h3>

<p>The obligations created by these standards are real and time-bound:</p>

<ul>
  <li><strong>B2G (suppliers to federal government):</strong> mandatory since
<a href="https://www.e-rechnung-bund.de/faq/xrechnung/">27 November 2020</a> at federal level; most
Bundesländer have since followed.</li>
  <li><strong>B2B receiving:</strong> mandatory since <a href="https://www.bundesfinanzministerium.de/">1 January 2025</a>
under §14 UStG (Wachstumschancengesetz) — every German B2B company must be able to process
EN 16931-compliant e-invoices.</li>
  <li><strong>B2B sending:</strong> mandatory from <a href="https://www.bundesfinanzministerium.de/">1 January 2027</a>
(with transitional rules).</li>
</ul>

<p>There is <strong>no KoSIT certification scheme for software vendors</strong>. Conformance is defined
operationally: if your output passes the KoSIT validator, it is conformant. Invoices that fail
the validator are automatically rejected by government portals, which is a stronger forcing
function than any formal certification could be.</p>

<hr />

<h2 id="the-libraries-mustangproject-vs-zugferd-csharp">The Libraries: Mustangproject vs ZUGFeRD-csharp</h2>

<h3 id="mustangproject">mustangproject</h3>

<p><a href="https://github.com/ZUGFeRD/mustangproject">mustangproject</a> is a Java library, Maven artifact,
command-line tool, and REST server. It has been in development since the early days of ZUGFeRD
and has accumulated broad format support: ZUGFeRD 1 and 2.x, Factur-X, and XRechnung (CII and
UBL syntaxes).</p>

<p>A key architectural decision is that <strong>validation is built in</strong>. The library bundles the KoSIT
XRechnung Schematron rules directly (as listed in its
<a href="https://github.com/ZUGFeRD/mustangproject/blob/master/NOTICE">NOTICE file</a>), as well as EN
16931 validation artefacts from the CEN. This makes mustangproject the de facto reference
implementation for validation in the German e-invoicing open source ecosystem.</p>

<p>The project’s popularity in the Java ecosystem is partly structural: Java has a long history in
German ERP and accounting software (SAP, Lexware, Sage), so a library with a clean Maven
dependency fits naturally into existing stacks. The command-line tool also broadened the audience
beyond Java developers.</p>

<p>One caveat worth noting: an
<a href="https://github.com/ZUGFeRD/mustangproject/issues/661">open bug</a> shows a case where
mustangproject rejects XML that the KoSIT validator accepts — meaning it applies stricter rules
than the official reference in some edge cases. This may or may not be a problem depending on
your use case.</p>

<h3 id="zugferd-csharp">ZUGFeRD-csharp</h3>

<p><a href="https://github.com/stephanstapel/ZUGFeRD-csharp">ZUGFeRD-csharp</a> started in 2013 as a hobby
project and grew to be used by hundreds of companies. It remains the primary .NET option for
this domain.</p>

<p>However, it has recently undergone a significant licensing change.
<a href="https://github.com/stephanstapel/ZUGFeRD-csharp/releases">Starting with version 18.0.0</a>, the
project moves to an open-core model: the base library remains open source for stability and bug
fixes, but new features and — notably — <strong>validation</strong> are now part of the commercial
<a href="https://www.factoorsharp.de/">FactoorSharp</a> product.</p>

<p>The commercial validation component is advertised as using Mustang, Valitool, and VeraPDF as
backends. This is worth noting for two reasons:</p>

<ol>
  <li>It means the .NET ecosystem’s primary validation path delegates to the Java library — Mustang
again acting as the ground truth.</li>
  <li>From a purely commercial perspective, advertising that your paid feature calls a free
competitor may not be the strongest value proposition, though the integration work and
convenience still has value.</li>
</ol>

<p>For teams currently using ZUGFeRD-csharp, version 18 remains available as open source, but
future XRechnung version support and new features will likely appear first or only in the
commercial tier. This is a risk worth factoring into dependency decisions.</p>

<hr />

<h2 id="why-net-validation-delegates-to-java-the-xslt-20-gap">Why .NET Validation Delegates to Java: The XSLT 2.0 Gap</h2>

<p>The decision to call Mustang from the commercial .NET validation tier is not arbitrary — it
reflects a structural platform limitation that is worth understanding.</p>

<p>The KoSIT Schematron validation rules are compiled to <strong>XSLT 2.0</strong>. The built-in .NET XML stack
(<code class="language-plaintext highlighter-rouge">XslCompiledTransform</code>) only supports XSLT 1.0. As one developer put it plainly in the
ZUGFeRD-csharp issue tracker, <code class="language-plaintext highlighter-rouge">document-uri()</code> — an XSLT 2.0 function used in the KoSIT
stylesheets — is simply not recognised, making native .NET validation impossible without an
external processor.</p>

<p>The natural solution is <a href="https://github.com/Saxonica/Saxon-HE">Saxon-HE</a>, the open source XSLT
3.0 processor from Saxonica, available as a NuGet package. However, here the platform
fragmentation becomes painful:</p>

<ul>
  <li><strong>Saxon-HE for .NET Framework</strong> works and has been demonstrated in community contributions
to ZUGFeRD-csharp, but ZUGFeRD-csharp targets <strong>.NET Standard</strong>, not .NET Framework.</li>
  <li><strong>SaxonCS</strong> — the modern .NET Standard/Core-compatible version — is a <strong>commercial product</strong>,
not open source, and was described in the ZUGFeRD-csharp community as unaffordably expensive
for an open source library dependency.</li>
</ul>

<p>This is corroborated by our own experiments, which reached the same conclusion independently.</p>

<p>The net result is a somewhat uncomfortable situation for the .NET e-invoicing ecosystem: the
only practical path to free, standards-compliant XRechnung validation on modern .NET is to
shell out to a Java process running Mustang — which is exactly what the commercial FactoorSharp
tier does. There is also
<a href="https://github.com/stephanstapel/ZUGFeRD-csharp/discussions/316">concern in the community</a>
that Saxonica’s near-monopoly on XSLT 2.0/3.0 processing creates a long-term open source risk,
since the Java ecosystem’s Saxon-HE (also MPL-2.0) could in principle face the same commercial
pressure at any time.</p>

<p>For Java, this problem does not arise: Saxon-HE on the JVM is fully open source, supports XSLT
3.0, and integrates cleanly — which is one more structural reason why mustangproject can offer
validation as a first-class feature while ZUGFeRD-csharp cannot, at least not without
commercial dependencies.</p>

<hr />

<h2 id="three-ironies--and-a-hidden-fourth">Three Ironies — and a Hidden Fourth</h2>

<p>Stepping back, the landscape described in this post contains a set of structural ironies that
are worth naming explicitly, because together they explain a lot about why the ecosystem looks
the way it does.</p>

<p><strong>The first irony</strong> is commercial: ZUGFeRD-csharp’s paid validation tier advertises that it
calls <a href="https://github.com/ZUGFeRD/mustangproject">Mustang</a> — a free Java competitor — as one
of its backends. Whatever the integration value, announcing this publicly tells potential
customers that the core capability is available for free elsewhere.</p>

<p><strong>The second irony</strong> is a platform one: .NET, the ecosystem being used to implement a legally
mandatory invoicing standard, natively supports only XSLT 1.0 — while the KoSIT validation
rules that define conformance with that standard are written in XSLT 2.0. The platform and the
standard passed each other in opposite directions, and nobody built the bridge. The result is
that the .NET library’s commercial validation tier ends up shelling out to a Java process, which
is the one mainstream platform where the bridge was in fact built.</p>

<p><strong>The third irony</strong> is a browser one. Chrome has officially deprecated
XSLT, with removal from stable releases planned for November 2026 — and Firefox and WebKit have
indicated plans to follow. The stated reason is security: 
<code class="language-plaintext highlighter-rouge">libxslt</code> was unmaintained for roughly six months of 2025, and as an aging C codebase it
represents a potent attack surface for a feature that is now rarely used on the web.
The XSLT stylesheets bundled in both libraries for generating human-readable HTML invoice views
will stop working in browsers that do this rendering client-side. To be clear, this does
<em>not</em> affect server-side validation or rendering pipelines — mustangproject, the KoSIT validator,
and any server-side workflow are unaffected. But it is a reminder that the browser ecosystem,
which never advanced beyond XSLT 1.0 either, is now actively retreating even from that.</p>

<p><strong>The hidden fourth irony</strong> underlies all three. XSLT 2.0 was published as a W3C recommendation
in 2007. The broader platform ecosystem — browsers, .NET, and others — largely chose not to
implement it. The standards bodies building XRechnung validation on XSLT 2.0 Schematron were
making a reasonable technical bet on a published W3C standard; the platform world had quietly
decided not to honour it. Saxonica filled the gap and now holds something close to a monopoly
on XSLT 2.0/3.0 processing, with all the open source sustainability risk that entails. Java
happens to be the platform where Saxonica’s open source tier works cleanly — and so mustangproject,
almost incidentally, ended up on the right side of a technology adoption decision made twenty
years ago by people who were thinking about something else entirely.</p>

<hr />

<h2 id="summary">Summary</h2>

<table>
  <thead>
    <tr>
      <th>Topic</th>
      <th>Observation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>scc / LOC</td>
      <td>Useful for locating where project mass lives; useless for effort estimation when standards artefacts dominate</td>
    </tr>
    <tr>
      <td>COCOMO estimates</td>
      <td>Not meaningful here; treat as noise</td>
    </tr>
    <tr>
      <td>KoSIT test suite</td>
      <td>Normative, versioned, government-backed — exists for XRechnung only</td>
    </tr>
    <tr>
      <td>ZUGFeRD test suite</td>
      <td>No official equivalent; community corpus exists but is unofficial</td>
    </tr>
    <tr>
      <td>mustangproject</td>
      <td>Java, fully open source, validation built in via KoSIT Schematron</td>
    </tr>
    <tr>
      <td>ZUGFeRD-csharp</td>
      <td>.NET, moving to open-core from v18; validation in commercial tier</td>
    </tr>
    <tr>
      <td>XSLT 2.0 on .NET</td>
      <td>Built-in stack is XSLT 1.0 only; Saxon-HE requires .NET Framework; SaxonCS is commercial — structural barrier to free validation on modern .NET</td>
    </tr>
    <tr>
      <td>Browser XSLT removal</td>
      <td>Chrome removing XSLT from stable in Nov 2026; Firefox and WebKit to follow — affects client-side rendering only, not server-side validation</td>
    </tr>
    <tr>
      <td>XRechnung vs ZUGFeRD</td>
      <td>Different formats, different governance; XRechnung is legally mandatory for B2G</td>
    </tr>
    <tr>
      <td>B2B obligation</td>
      <td>Receiving mandatory since Jan 2025; sending from Jan 2027</td>
    </tr>
    <tr>
      <td>“Certified by KoSIT”</td>
      <td>No such scheme exists; passing the KoSIT validator is the conformance test</td>
    </tr>
  </tbody>
</table>

<hr />

<p><em>Corrections and additions welcome. This post reflects a learning process rather than expert
knowledge, and the e-invoicing standards landscape changes at least once a year with each KoSIT
release cycle.</em></p>]]></content><author><name></name></author><category term="e-invoicing" /><category term="open-source" /><category term="code-quality" /><category term="standards" /><summary type="html"><![CDATA[A developer’s notes on code metrics, ZUGFeRD, XRechnung, and the libraries that implement them.]]></summary></entry><entry><title type="html">Getting Jekyll Running on Windows: 99 Gems and One Version Mismatch</title><link href="https://goerch.github.io/jekyll/update/2026/04/26/getting_jekyll_running_on_windows.html" rel="alternate" type="text/html" title="Getting Jekyll Running on Windows: 99 Gems and One Version Mismatch" /><published>2026-04-26T17:11:29+00:00</published><updated>2026-04-26T17:11:29+00:00</updated><id>https://goerch.github.io/jekyll/update/2026/04/26/getting_jekyll_running_on_windows</id><content type="html" xml:base="https://goerch.github.io/jekyll/update/2026/04/26/getting_jekyll_running_on_windows.html"><![CDATA[<p>So you want to publish a static blog with Jekyll and GitHub Pages. How hard can it be?</p>

<p>Harder than it should be, it turns out — but not for the reasons you’d expect.</p>

<p><strong>Starting point</strong></p>

<p>Fresh Windows install, Ruby 4.0.3 via winget, following the official GitHub Pages documentation. Step one: create a new Jekyll site, add the <code class="language-plaintext highlighter-rouge">github-pages</code> gem, run <code class="language-plaintext highlighter-rouge">bundle install</code>. Straightforward enough.</p>

<p>Except it isn’t.</p>

<p><strong>The version wall</strong></p>

<p><code class="language-plaintext highlighter-rouge">bundle install</code> fails immediately with a dependency error: <code class="language-plaintext highlighter-rouge">commonmarker</code>, somewhere deep in the <code class="language-plaintext highlighter-rouge">github-pages</code> gem chain, explicitly requires Ruby <code class="language-plaintext highlighter-rouge">&gt;= 2.6, &lt; 4.0</code>. Ruby 4.0.3 is, technically, not less than 4.0.</p>

<p>The GitHub documentation points you to <code class="language-plaintext highlighter-rouge">pages.github.com/versions.json</code> for the current gem version but says nothing about Ruby version requirements. To be fair, pinning documentation to fast-moving version constraints is genuinely hard — but a note like “Ruby 3.x recommended” would save a lot of people some time.</p>

<p>The fix: uninstall Ruby 4.0.3, install 3.4.x via RubyInstaller (with Devkit — don’t skip that), start over.</p>

<p><strong>99 gems</strong></p>

<p>Once the right Ruby is in place, <code class="language-plaintext highlighter-rouge">bundle install</code> completes cheerfully:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bundle complete! 7 Gemfile dependencies, 99 gems installed.
</code></pre></div></div>

<p>99 gems. For a static site generator. If you know the song, you know the song.</p>

<p>To be fair to Jekyll: the resulting site looks genuinely nice out of the box, Minima is a clean default theme, and the whole thing is now running on localhost exactly as advertised. The friction was entirely in getting there.</p>

<p><strong>What I’d do differently</strong></p>

<p>Install Ruby via RubyInstaller from the start, not winget. Pick 3.3.x or 3.4.x. Run <code class="language-plaintext highlighter-rouge">ridk install</code> when it asks you to. Then follow the docs.</p>

<p>Two hours of dependency archaeology, condensed into one paragraph.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[So you want to publish a static blog with Jekyll and GitHub Pages. How hard can it be?]]></summary></entry></feed>