|
9 Min. Lesezeit
|

@atomic-ehr/codegen: FHIR-Pakete reparieren, ohne auf Upstream zu warten

Diesen Artikel zusammenfassen mit:
ChatGPTPerplexityClaudeGrok

FHIR-Pakete sind das Distributionsformat der FHIR-Welt: npm-artige Tarballs mit StructureDefinitions, ValueSets und CodeSystems. Fast jedes Werkzeug liest sie – FHIR- und Terminology-Server, Validatoren, der IG Publisher, SDK-Generatoren usw.

Das Format ist einfach, die Registries sind offen, und der Inhalt entspricht häufig nicht dem, was ein Werkzeug benötigt. Die Registry enthält mehr als tausend Pakete, von nationalen Programmen, Herstellern, Projektteams und HL7 selbst, in eigenen Veröffentlichungsrhythmen und über mehrere Standard-Releases hinweg. Dass dabei Fehler entstehen, war absehbar.

@atomic-ehr/codegen kompiliert diese Definitionen zu typisierten SDKs, und die Zielsprachen sind wenig nachsichtig. TypeScript, C# und Python/Pydantic erlauben keine hängenden Referenzen: Jeder Typ muss existieren, jeder Enum-Wert muss ein echter Code sein und jede Ableitung muss die Definition einschränken. Damit codegen funktioniert, muss die gesamte Paketmenge statisch konsistent sein.

Was bei FHIR-Paketen in der Praxis zu erwarten ist

Jedes Beispiel stammt aus einem Paket, das Sie heute in der genannten Version installieren können. Die Fehler lassen sich drei Kategorien zuordnen, und jede bricht eine andere Build-Phase: das Laden der Pakete, das Auflösen einer Referenz und das Anwenden einer Einschränkung.

1. Fehlerhafte Paket-Metadaten

Manifest und Index des Pakets sind fehlerhaft, sodass der Loader die Paketmenge gar nicht erst zusammenstellen kann.

Ein fehlerhafter oder fehlender Index. Die Spezifikation verlangt eine .index.json-Datei, die Canonicals auf Dateinamen abbildet, damit Werkzeuge nicht jede Datei öffnen müssen. Manche Pakete werden ohne diese Datei ausgeliefert (ehelse.fhir.no.grunndata@2.3.5, hl7.fhir.no.basis@2.2.2), andere verweisen auf Dateien, die nicht vorhanden sind (de.basisprofil.r4@1.6.0-ballot2).

Eine fehlende Core-Abhängigkeit. Die Spezifikation ist eindeutig – „Eine Abhängigkeit von einem Core-Paket ist erforderlich" – dennoch lassen viele Pakete diese aus (simplifier.core.r4* bei 4.0.x, ehelse.fhir.no.grunndata@2.3.5, nhn.fhir.no.kjernejournal@1.0.0, sfm.030322@2.0.1), obwohl sie durchgängig R4-Core-Typen verwenden.

Ein Manifest, das nicht mit der Registry übereinstimmt. simplifier.core.r4.base@4.0.0 hängt von simplifier.core.r4.resources ab. Die Registry liefert dieses Paket aus, aber die package.json im Tarball besagt:

{ "name": "simplifier.core.r4.rResources", "version": "4.0.0" }

2. Eine Referenz, die nicht aufgelöst werden kann

Die Definition verweist auf etwas, das die Paketmenge nicht enthält.

Ein Canonical, den niemand definiert. In hl7.cda.uv.core@2.0.2-sd ist Material.sdtcExpirationTime als .../StructureDefinition/IVL_TS typisiert – mit Unterstrich. Der Datentyp ist unter .../IVL-TS mit Bindestrich veröffentlicht, und kein Paket deklariert die Unterstrich-Schreibweise.

Ein Datentyp aus einer anderen FHIR-Version. hl7.fhir.uv.extensions.r4 ist HL7s Extensions Pack für R4 – das Manifest gibt "fhirVersions": ["4.0.1"] an. Drei Jahre lang wurden Extensions ausgeliefert, deren value[x] die Typen CodeableReference oder Availability verwendeten – Datentypen, die in R5 hinzugefügt wurden und in 4.0.1 nicht existieren:

PaketVeröffentlichtExtensions mit ausschließlich R5-Datentypen
hl7.fhir.uv.extensions.r4@1.0.0März 202310
hl7.fhir.uv.extensions.r4@5.1.0Apr. 20249
hl7.fhir.uv.extensions.r4@5.2.0Feb. 20258
hl7.fhir.uv.extensions.r4@5.3.0Mai 20260

Das Problem wurde in 5.3.0 (Mai 2026) behoben. Ältere Versionen sind weiterhin über Paketabhängigkeiten im Einsatz. Vier Pakete im C-CDA-Closure deklarieren extensions.r4, und jedes davon verweist auf eine fehlerhafte Version: hl7.fhir.us.core, hl7.fhir.uv.xver-r5.r4 und hl7.terminology.r4 fordern 5.2.0, während hl7.terminology noch 1.0.0 verlangt. Die korrigierte Version selbst anzufordern ändert nichts – jede Deklaration muss mit ensureDependency umgeleitet werden, wie das C-CDA-Beispiel zeigt.

3. Ein Profil, das seiner Basis widerspricht

Die Paketierung ist korrekt und jeder Canonical lässt sich auflösen. Die Einschränkung selbst ist nicht zulässig – ein Profil darf nur einschränken, was seine Basis bereits erlaubt. Einige Beispiele:

Es erweitert statt einzuschränken. Basis-R4 erlaubt es, dass RelatedPerson.patient auf genau eine Sache verweist:

// hl7.fhir.r4.core@4.0.1 — RelatedPerson.patient
"type": [{
  "code": "Reference",
  "targetProfile": ["http://hl7.org/fhir/StructureDefinition/Patient"]
}]

Das norwegische gd-RelatedPerson leitet davon ab und listet fünf:

// ehelse.fhir.no.grunndata@2.3.5 — gd-RelatedPerson, same element
"type": [{
  "code": "Reference",
  "targetProfile": [
    "http://hl7.org/fhir/StructureDefinition/Patient",
    "http://hl7.org/fhir/StructureDefinition/Person",
    // ...
  ]
}]

Es legt einen Wert fest, der nicht existiert. Das bundle-type-CodeSystem definiert genau zehn Codes:

// hl7.fhir.r5.core@5.0.0 — CodeSystem/bundle-type
"concept": [
  { "code": "document" },    { "code": "message" },
  { "code": "transaction" }, { "code": "transaction-response" },
  { "code": "batch" },       { "code": "batch-response" },
  { "code": "history" },     { "code": "searchset" },
  { "code": "collection" },  { "code": "subscription-notification" }
]

R5 Cores eigenes batch-bundle-Profil legt Bundle.type auf einen elften Code fest:

// hl7.fhir.r5.core@5.0.0 — StructureDefinition/batch-bundle
{ "id": "Bundle.type", "path": "Bundle.type", "patternCode": "bundle" }

Beide Auswege sind versperrt. Den patternCode zu korrigieren stößt auf HL7s Regelwerk: „Festgelegte Werte und Muster werden nicht in einer Weise hinzugefügt, entfernt oder geändert, die bestehende Instanzen ungültig machen könnte." bundle zum CodeSystem hinzuzufügen ist nicht besser – bundle-type ist normativ und als content: "complete" markiert. In beiden Fällen ist ein Ballot erforderlich, kein Patch.

Warum das Warten auf Upstream keine Option ist

Reichen Sie trotzdem Tickets ein – die Fehler, die behoben wurden, wurden behoben, weil jemand ein Ticket eingereicht hat. Aber Upstream kann nicht Ihr einziger Plan sein.

Geschwindigkeit. FHIR strebt Releases „ungefähr alle 18–24 Monate" an. Die tatsächliche Lücke zwischen R4 und R5 betrug 51 Monate. Ein heute akzeptiertes Ticket landet in einer Spezifikation, die noch nicht existiert, während Ihr Build jetzt funktionieren muss.

Unveränderlichkeit. HL7 hat es in einem Sicherheitshinweis erklärt, in dem erläutert wurde, warum ein Fix bereits veröffentlichte Inhalte nicht erreichen konnte: „Veröffentlichte Pakete sind unveränderlich." Ein Fix wird also stets als neue Version ausgeliefert, und ob Sie darauf wechseln, liegt oft nicht in Ihrer Hand – ein nationales Programm pinnt eine Ballot-Version, US-Regulierung nennt IG-Versionen in 45 CFR § 170.215. Manchmal gibt es schlicht keine neuere Version: hl7.fhir.r4.core hatte seit Oktober 2019 nur eine einzige Veröffentlichung: 4.0.1.

Unmöglichkeit. Und manche Fehler haben gar keinen Upstream-Fix – die Korrektur von batch-bundle bedeutet, normativen Inhalt wieder zu öffnen, was genau das ist, was der normative Status verhindert.

Pakete im eigenen Build reparieren

Warten bedeutet also, auf den Herausgeber des benötigten Pakets und auf die Herausgeber aller seiner Abhängigkeiten zu warten. Die Reparatur muss stattdessen auf Ihrer Seite erfolgen. Aber genau wo?

Den entpackten Tarball bearbeiten. Das ist eine verbreitete und einfache Methode: Datei ändern, Build erneut ausführen. Doch die Änderung wird nicht festgehalten, die nächste Installation überschreibt sie, und andere Werkzeuge können das modifizierte Paket lesen, ohne zu wissen, dass es verändert wurde.

Das Paket forken, vendorn oder neu hosten. Sie sind nun an die Version gebunden, die Sie geforkt haben, und jedes Upstream-Release muss von Hand erneut geforkt oder stillschweigend übersprungen werden. Ihre Kopie ersetzt außerdem nicht das Original: Alles andere im Closure, das noch vom echten Paket abhängt, zieht es neben Ihrer Version ein, und dieselben Canonicals landen doppelt in der Definition.

Den Fehler als Sonderfall im Code-Generator behandeln. Jetzt lebt eine Tatsache über das Paket eines anderen in Ihrem Quellcode, wo niemand danach suchen würde.

Codegen wählt den verbleibenden Weg: Der Fix wird in Ihrer Codegen-Konfiguration deklariert, und das Paket wird durch diese gefiltert gelesen.

Was Sie patchen können

Patches werden im Loader angewendet, bevor der Generator irgendetwas sieht.

package.json .index.json resources FHIR packageas published packageJsonensureDependency · renamePackage indexEntryexcludeCanonical fhirResourcereplaceText · ensureCodes patched copyin memory only

Fünf davon, bezogen auf die zuvor beschriebenen Fehler:

patches: {
  packageJson: [
    // Most Norge packages use R4 core types without declaring the dependency.
    ensureDependency({ "hl7.fhir.r4.core": "4.0.1" }),
    // Published as .resources, but calls itself .rResources in its own manifest.
    renamePackage("simplifier.core.r4.rResources", "simplifier.core.r4.resources"),
  ],
  indexEntry: [
    // shareablecodesystem's recursive concept.concept breaks the schema generator.
    excludeCanonical({
      package: { name: "hl7.fhir.r5.core", version: "5.0.0" },
      url: "http://hl7.org/fhir/StructureDefinition/shareablecodesystem",
      reason: "CodeSystem.concept.concept recursion breaks generation",
    }),
  ],
  fhirResource: [
    // Material.sdtcExpirationTime points at .../IVL_TS; the datatype is published
    // at .../IVL-TS. Scoped to Material, because IVL-TS's own `type` field carries
    // the underscore spelling legitimately — an unscoped replace would break it.
    inResource("http://hl7.org/cda/stds/core/StructureDefinition/Material", [
      replaceText(
        "http://hl7.org/cda/stds/core/StructureDefinition/IVL_TS",
        "http://hl7.org/cda/stds/core/StructureDefinition/IVL-TS",
      ),
    ]),
    // bundle-type is missing codes the R5 bundle profiles pin — including
    // "bundle", batch-bundle's typo, which is not a legal code anywhere.
    ensureCodes("http://hl7.org/fhir/bundle-type", ["bundle", "subscription-notification"]),
  ],
}

Drei Dinge sollten Sie kennen, bevor Sie einen Patch schreiben.

Der Ort eines Patches ist entscheidend. Manifest, Index und Ressource-Inhalte werden zu unterschiedlichen Zeitpunkten gelesen, daher muss ein Tippfehler im Manifest in packageJson behoben werden – wenn die Ressourcen gelesen werden, ist der Abhängigkeitsgraph bereits aufgebaut.

Schränken Sie einen Patch auf sein Ziel ein. Ihn mit inPackage oder inResource zu umschließen, beschränkt ihn auf ein einzelnes Paket oder einen einzelnen Canonical. Das ist optional, aber in der Regel sinnvoll. Das obige ensureCodes läuft ohne Einschränkung – das ist in Ordnung, da es nur das genannte CodeSystem berührt.

Ein Ausschluss entfernt mehr als nur die Ausgabe. Ein ausgeschlossener Canonical verschwindet auch aus der Auflösung, sodass alles, was davon abgeleitet ist, ebenfalls entfernt werden muss.

Zwei dieser Aufrufe übernimmt codegen für Sie:

  • Bekannte HL7-Fehler werden standardmäßig gepatcht. builtinPatches enthält sie und wendet sie bei jedem Build an, sodass niemand sie neu entdecken muss. Sie können sie deaktivieren oder ersetzen, wenn Sie anderer Meinung sind.
  • Ein Paket ohne Index erhält automatisch einen. Er wird aus einem Verzeichnis-Scan erstellt. Ein vorhandener, aber fehlerhafter Index wird mit einer Warnung in Ruhe gelassen, bis Sie explizit packageIndex: "recover" anfordern – einen Index neu zu erstellen, den ein Herausgeber ausgeliefert hat, ist eine größere Annahme, die nicht stillschweigend getroffen werden sollte.

Mit Patches leben

Ein Patch verändert die Daten eines anderen, daher landet jede angewendete Korrektur im Generierungsbericht – jeder wiederhergestellte Index und jeder Ausschluss, zusammen mit dem reason, den der Patch deklariert hat. „Warum unterscheidet sich mein generiertes SDK vom veröffentlichten Paket?" hat damit immer eine Antwort, und eine, die jeder nachprüfen kann. Das ist das eigentliche Argument für das Deklarieren einer Korrektur statt für das manuelle Eingreifen: Ein bearbeiteter Tarball meldet nichts, ein geforktes Paket erklärt nichts, während ein Patch angeben muss, was er getan hat und warum. Drei Regeln halten diesen Ansatz wertvoll.

Reichen Sie trotzdem Upstream-Tickets ein. Ein Patch kauft Zeit; er ist kein Fix.

Reparieren Sie die Eingabe, gestalten Sie sie nicht um. Der obige bundle-type-Patch ist die schnelle Lösung – er fügt einen Code hinzu, den HL7 nie definiert hat, anstatt den patternCode von batch-bundle zu korrigieren.

Rechnen Sie damit, Patches zu löschen. Diese acht Ausschlüsse wurden nutzlos, als 5.3.0 veröffentlicht wurde, und codegen hat es nicht bemerkt. Ein Patch, der stillschweigend nichts tut, sieht genauso aus wie einer, der wichtige Arbeit leistet.


Was Sie über ein fehlerhaftes Paket wissen, gehört in die Konfiguration, wo es wie alles andere versioniert und überprüft wird – nicht in eine Kopie des Pakets, die Sie nun selbst pflegen, nicht in den Quellcode Ihres Generators, nicht in eine Wiki-Seite, die drei Personen kennen. Deklarieren Sie es einmal, mit Begründung, und reichen Sie das Upstream-Ticket ein, während Ihr Build grün bleibt.

Jeder hier genannte Fehler wurde im September 2026 anhand der veröffentlichten Tarballs in den angegebenen Versionen überprüft. Die Konfiguration stammt aus den Beispiel-Pipelines von codegen, die bei jedem Push gegen Live-Registries neu generiert und typgeprüft werden.

Was codegen aufbaut, wenn die Eingabe korrekt ist, erfahren Sie in US Core-Profile in TypeScript und Type Schema: ein pragmatischer Ansatz zum Aufbau von FHIR SDKs.

GitHub | NPM | Aleksandr Penskoi auf LinkedIn

Diesen Artikel teilen
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

Get the latest articles on FHIR, interoperability, and healthcare IT.