|
9 min de lectura
|

@atomic-ehr/codegen: Cómo corregir paquetes FHIR sin esperar al origen

Resumir este artículo con:
ChatGPTPerplexityClaudeGrok

Los paquetes FHIR son el mecanismo con el que el ecosistema FHIR distribuye contenido: tarballs al estilo npm que contienen StructureDefinitions, ValueSets y CodeSystems. Prácticamente todas las herramientas los leen: servidores FHIR y de terminología, validadores, el IG Publisher, generadores de SDK, etc.

El formato es sencillo, los registros son abiertos y el contenido no siempre es lo que una herramienta necesita. El registro aloja más de mil paquetes procedentes de programas nacionales, proveedores, equipos de proyecto y el propio HL7, cada uno con su propio calendario y abarcando varias versiones del estándar. Era inevitable que algunos tuviesen defectos.

@atomic-ehr/codegen compila esas definiciones en SDKs tipados, y los lenguajes de destino no admiten errores. TypeScript, C# y Python/Pydantic no permiten referencias colgantes: cada tipo debe existir, cada valor de enumeración debe ser un código real y, además, cada derivación debe restringir la definición. Para que codegen funcione, el conjunto completo de paquetes debe ser consistente de forma estática.

Qué cabe esperar de los paquetes FHIR en entornos reales

Cada ejemplo pertenece a un paquete que puede instalarse hoy, en la versión indicada. Los defectos se clasifican en tres tipos, y cada uno rompe una etapa diferente de la compilación: la carga de los paquetes, la resolución de una referencia o la aplicación de una restricción.

1. Metadatos de paquete incorrectos

El manifiesto y el índice son erróneos, por lo que el cargador no puede ni siquiera ensamblar el conjunto de paquetes.

Un índice roto o ausente. La especificación exige un fichero .index.json que mapea los canonicals a los nombres de fichero para que las herramientas no tengan que abrir cada fichero individualmente. Algunos paquetes se distribuyen sin él (ehelse.fhir.no.grunndata@2.3.5, hl7.fhir.no.basis@2.2.2), y otros apuntan a ficheros inexistentes (de.basisprofil.r4@1.6.0-ballot2).

Una dependencia de núcleo ausente. La especificación es explícita — «se requiere una dependencia de un paquete de núcleo» —, pero muchos paquetes la omiten (simplifier.core.r4* en 4.0.x, ehelse.fhir.no.grunndata@2.3.5, nhn.fhir.no.kjernejournal@1.0.0, sfm.030322@2.0.1) mientras usan tipos del núcleo R4 a lo largo de todo su contenido.

Un manifiesto que no concuerda con el registro. simplifier.core.r4.base@4.0.0 depende de simplifier.core.r4.resources. El registro sirve ese paquete, pero el package.json dentro del tarball dice:

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

2. Una referencia que no se resuelve

La definición apunta a algo que el conjunto de paquetes no contiene.

Un canonical que nadie define. En hl7.cda.uv.core@2.0.2-sd, Material.sdtcExpirationTime está tipado como .../StructureDefinition/IVL_TS — con guión bajo. El tipo de dato se publica en .../IVL-TS, con guión, y ningún paquete declara la versión con guión bajo.

Un tipo de dato de otra versión de FHIR. hl7.fhir.uv.extensions.r4 es el Extensions Pack de HL7 para R4 — su manifiesto indica "fhirVersions": ["4.0.1"]. Durante tres años distribuyó extensiones cuyo value[x] usaba CodeableReference o Availability, tipos de datos añadidos en R5 que no existen en 4.0.1:

PaquetePublicadoExtensiones tipadas con tipos de datos exclusivos de R5
hl7.fhir.uv.extensions.r4@1.0.0mar. 202310
hl7.fhir.uv.extensions.r4@5.1.0abr. 20249
hl7.fhir.uv.extensions.r4@5.2.0feb. 20258
hl7.fhir.uv.extensions.r4@5.3.0may. 20260

El problema se resolvió en la versión 5.3.0, publicada en mayo de 2026. Las versiones anteriores siguen en uso a través de dependencias de paquetes. Cuatro paquetes del cierre de C-CDA declaran extensions.r4, y todos ellos apuntan a una versión defectuosa: hl7.fhir.us.core, hl7.fhir.uv.xver-r5.r4 y hl7.terminology.r4 solicitan 5.2.0, mientras que hl7.terminology sigue pidiendo 1.0.0. Solicitar usted mismo la versión corregida no cambia nada — cada declaración debe redirigirse con ensureDependency, tal como muestra el ejemplo de C-CDA.

3. Un perfil que contradice su base

El empaquetado es correcto y todos los canonicals se resuelven. La restricción en sí no es válida — un perfil solo puede restringir lo que su base ya permite. Veamos algunos ejemplos:

Amplía en lugar de restringir. El R4 base permite que RelatedPerson.patient apunte exactamente a un elemento:

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

El perfil noruego gd-RelatedPerson deriva de él y lista cinco:

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

Fija un valor que no existe. El CodeSystem bundle-type define exactamente diez códigos:

// 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" }
]

El propio perfil batch-bundle del núcleo R5 fija Bundle.type a un undécimo código:

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

Ambas salidas están bloqueadas. Corregir el patternCode choca con las normas de HL7: «los valores fijos y los patrones no se añadirán, eliminarán ni modificarán de forma que pueda invalidar instancias anteriores». Añadir bundle al CodeSystem tampoco es mejor — bundle-type es normativo y está marcado como content: "complete". Cualquiera de las dos opciones requiere una votación, no un parche.

Por qué esperar al origen no es viable

Abra los tickets de todas formas — los defectos que se corrigieron se corrigieron porque alguien los reportó. Pero el proceso ascendente no puede ser su único plan.

Velocidad. FHIR aspira a publicar una nueva versión «aproximadamente cada 18-24 meses». El intervalo real entre R4 y R5 fue de 51 meses. Un ticket aceptado hoy aterrizará en una especificación que aún no existe, mientras que su compilación tiene que pasar ahora mismo.

Inmutabilidad. HL7 lo declaró en un aviso de seguridad, explicando por qué una corrección no podía llegar al contenido ya publicado: «los paquetes publicados son inmutables». Por tanto, una corrección siempre se distribuye como una nueva versión, y la decisión de migrar a ella no siempre depende de usted — un programa nacional fija una versión ballot, la regulación estadounidense nombra versiones de IG en 45 CFR § 170.215. En ocasiones no existe ninguna versión más reciente: hl7.fhir.r4.core ha tenido una única publicación, 4.0.1, en octubre de 2019.

Imposibilidad. Y algunos defectos no tienen solución posible en el origen — corregir batch-bundle implica reabrir contenido normativo, que es precisamente lo que el estado normativo impide.

Reparar paquetes en su propia compilación

Esperar, entonces, significa esperar al publicador del paquete que necesita y a los publicadores de todo aquello de lo que depende. La reparación tiene que ocurrir en su lado. Pero, ¿exactamente dónde?

Editar el tarball desempaquetado. Es una solución habitual y directa: cambie el fichero y vuelva a ejecutar la compilación. Pero la edición no queda registrada, la próxima instalación la sobreescribe y otras herramientas pueden leer el paquete modificado sin saber que fue alterado.

Hacer un fork, vendorizar o realojar el paquete. Queda vinculado a la versión que bifurcó, y cada nueva versión publicada en el origen debe re-bifurcarse manualmente o ignorarse sin más. Su copia tampoco reemplaza al original: cualquier otra cosa del cierre que aún dependa del paquete real lo descargará junto al suyo, y los mismos canonicals acabarán definidos dos veces.

Tratar el defecto como caso especial en su generador de código. Ahora un hecho sobre el paquete de otra persona vive en su código fuente, donde nadie pensará en buscarlo.

Codegen adopta la vía restante: la corrección se declara en la configuración de codegen y el paquete se lee a través de ella.

Qué puede parchearse

Los parches se aplican en el cargador, antes de que el generador vea nada.

package.json .index.json resources Paquete FHIRtal como se publica packageJsonensureDependency · renamePackage indexEntryexcludeCanonical fhirResourcereplaceText · ensureCodes copia parcheadasolo en memoria

Cinco de ellos, aplicados a los defectos descritos anteriormente:

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"]),
  ],
}

Hay tres aspectos que conviene conocer antes de escribir un parche.

El lugar donde se aplica un parche importa. El manifiesto, el índice y los cuerpos de los recursos se leen en momentos distintos, por lo que una errata en el manifiesto debe corregirse en packageJson — cuando se leen los recursos, el grafo de dependencias ya está construido.

Acote el parche a su objetivo. Envolver uno en inPackage o inResource lo restringe a un único paquete o a un único canonical. Es opcional, pero casi siempre merece la pena. El ensureCodes anterior se aplica sin ámbito, lo cual es correcto — solo afecta al CodeSystem que nombra.

La exclusión elimina más que la salida. Un canonical excluido desaparece también de la resolución, por lo que cualquier elemento derivado de él debe excluirse igualmente.

Dos de estas llamadas las realiza codegen por usted:

  • Los defectos conocidos de HL7 se parchean por defecto. builtinPatches los incluye y los aplica en cada compilación, de modo que nadie tiene que redescubrirlos. Desactívelos o sustitúyalos si no está de acuerdo.
  • Un paquete que se distribuye sin índice recibe uno automáticamente. Se construye mediante un escaneo de directorio, de forma automática. Un índice que existe pero es incorrecto se deja como está con una advertencia hasta que usted solicite packageIndex: "recover" — reconstruir algo que un publicador sí distribuyó es una suposición mayor que hacer en su nombre.

Convivir con los parches

Un parche modifica los datos de otra persona, por lo que cada corrección aplicada aparece en el informe de generación — cada índice recuperado y cada exclusión, con la indicación reason declarada en su parche. «¿Por qué mi SDK generado difiere del paquete publicado?» tiene así siempre una respuesta que cualquiera puede verificar. Ese es el verdadero argumento para declarar una reparación en lugar de hacerla a mano: un tarball editado no informa de nada y un paquete bifurcado no explica nada, mientras que un parche tiene que indicar qué hizo y por qué. Tres reglas mantienen ese valor.

Abra el ticket en el origen de todas formas. Un parche compra tiempo; no es una solución.

Repare la entrada, no la rediseñe. El parche de bundle-type anterior es la solución barata — añade un código que HL7 nunca definió en lugar de corregir el patternCode de batch-bundle.

Espere tener que eliminar parches. Esas ocho exclusiones se volvieron inútiles el día en que se publicó 5.3.0, y codegen no lo advirtió. Un parche que silenciosamente no hace nada tiene exactamente el mismo aspecto que uno que realiza un trabajo importante.


Lo que usted sabe sobre un paquete defectuoso pertenece a la configuración, donde se versiona y revisa como todo lo demás — no en una copia del paquete que ahora debe mantener, no en el código fuente de su generador, no en una página de wiki que tres personas conocen. Declárelo una vez, con su motivo, y abra el issue en el origen mientras su compilación sigue en verde.

Cada defecto mencionado aquí fue verificado en septiembre de 2026 contra los tarballs publicados en las versiones indicadas. La configuración proviene de los pipelines de ejemplo de codegen, que regeneran y comprueban los tipos contra registros en producción en cada push.

Para ver qué construye codegen una vez que la entrada es correcta, consulte Perfiles US Core en TypeScript y Type Schema: un enfoque pragmático para construir FHIR SDK.

GitHub | NPM | Aleksandr Penskoi en LinkedIn

Compartir este artículo
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

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