HealthChain valida FHIR? Pegou 80 de 180 defeitos e nenhum de conteúdo

O validate_resource() do HealthChain 0.16.1 reprovou 80 de 180 recursos FHIR sintéticos com defeito: os 80 de forma e 0 dos 100 de conteúdo, como um CPF com dígito errado.

Rogério Rodrigues

Rogério Rodrigues

Engenheiro de IA · mestrando em Informática em Saúde (UFSC)

Boletim de correção em papel pautado: 80 de 180 defeitos pegos pelo validate_resource() do HealthChain, todos de forma. Campo obrigatório, data, campo desconhecido e valor fora do conjunto 20 de 20; LOINC inventado, unidade UCUM, referência quebrada, CPF com dígito errado e invariante obs-6 0 de 20.

Passei 240 recursos FHIR sintéticos pelo validate_resource() do HealthChain 0.16.1. Eram 60 válidos e 180 com um defeito conhecido cada. Ele reprovou 80 dos 180: todos os 80 com defeito de forma, que são campo obrigatório ausente, data em dd/mm/aaaa, campo que não existe e gender fora do conjunto. Dos 100 com defeito de conteúdo, reprovou nenhum. Um CPF com dígito verificador errado passou como válido. Nos 60 recursos corretos, 0 falsos positivos.

Código, dados e saída gravada: demos/11-healthchain-validacao-fhir no nursia-research-lab.

Como rodei

  • HealthChain 0.16.1 (pip install healthchain==0.16.1), Apache-2.0, Python 3.11, CPU. Por baixo ele usa o fhir.resources 8.1.0. As duas versões estão fixadas no requirements.txt.
  • Gerei 240 recursos FHIR R4B com semente fixa: Patient, Observation de frequência cardíaca (LOINC 8867-4) e Condition (CID-10).
  • 60 são válidos e servem de controle. Os outros 180 têm um defeito cada, 20 por categoria, em 9 categorias. O defeito e a categoria são escolha minha. Os 20 casos de referência quebrada são Bundles (um Patient e uma Observation), o resto são recursos soltos.
  • Cada recurso passou por validate_resource(), que devolve um relatório com valid e a lista de problemas. Contei os reprovados (valid == False).

O CPF dos pacientes passa no dígito verificador, mas é sorteado e não pertence a ninguém. Nomes saem de uma lista curta inventada.

O que deu

Defeito colocado Tipo Reprovados
Campo obrigatório ausente forma 20 de 20
Data em dd/mm/aaaa forma 20 de 20
Campo que não existe no recurso forma 20 de 20
Valor fora do conjunto obrigatório (gender, status) forma 20 de 20
Código inventado sob o sistema LOINC conteúdo 0 de 20
Unidade que não é UCUM válida conteúdo 0 de 20
Referência pra um Patient que não está no Bundle conteúdo 0 de 20
CPF com dígito verificador errado conteúdo 0 de 20
Valor e dataAbsentReason juntos (invariante obs-6) conteúdo 0 de 20
Controle: recurso válido – 0 de 60

Forma: 80 de 80. Conteúdo: 0 de 100. Chamo de forma o que o modelo Pydantic ou o vínculo de code primitivo recusa, e de conteúdo o resto. Como o rótulo segue o que o módulo diz checar, o resultado confirma o docstring e não descobre nada novo. Tempo médio de cerca de 0,2 ms por recurso: a rodada gravada deu 0,17 ms, e entre rodadas e máquinas vi de 0,11 a 0,25 ms. O tempo muda a cada rodada, o resto é determinístico.

O que ele pega

Dois tipos de erro: o que o modelo Pydantic do fhir.resources recusa (campo ausente, data mal escrita, campo desconhecido) e o que o HealthChain confere em seguida, que são os valores de campos code com conjunto obrigatório. Um gender escrito “feminino” cai no segundo caso. Monto a saída a partir de expression, code e diagnostics do relatório:

Patient.gender (code-invalid)
Value 'feminino' is not in the required value set. Allowed values: male, female, other, unknown

A mensagem completa está em results/exemplo.txt. Cada problema vem com o caminho do campo (Patient.gender, Observation.status) e um código no vocabulário do OperationOutcome do FHIR. O relatório se converte em OperationOutcome com to_operation_outcome(). Na minha leitura, um agente que monta recurso FHIR consegue usar o caminho do campo pra corrigir o que errou. Não testei com agente.

O que ele deixa passar

Os cinco defeitos de conteúdo voltam como valid=True, com a lista de problemas vazia. Exemplo com dado brasileiro:

from healthchain.fhir import validate_resource

paciente = {
    "resourceType": "Patient",
    "identifier": [{
        "system": "http://www.saude.gov.br/fhir/r4/NamingSystem/cpf",
        "value": "44797837682",   # dígito verificador errado
    }],
}
rel = validate_resource(paciente)
print(rel.valid, rel.issues)    # True []

Esse é um dos CPFs com dígito errado da demo, e o validador não olhou o valor do identificador nos 20 casos. O mesmo vale pro código LOINC inventado, pra unidade fora do UCUM e pra referência a um Patient que não existe no Bundle. A invariante obs-6 (uma Observation com valor e motivo de ausência de dado) também passou nas 20.

O docstring de healthchain/fhir/validation.py lista o que não é checado: vínculos (bindings) de CodeableConcept e Coding, invariantes FHIRPath, conformidade com perfil e integridade de referência. Pra validação completa, o próprio módulo manda usar o $validate de um servidor FHIR. Três das cinco categorias de conteúdo caem nesses itens: o LOINC inventado, a referência quebrada e a obs-6. O CPF com dígito errado e a unidade fora do UCUM não estão na lista, são extensão minha. A demo mede esse recorte: 80 de 180 neste conjunto.

O que eu faria

  • Use o validate_resource() como porteiro de forma, na saída de um agente que gera FHIR. Ele é rápido e dá o caminho do erro.
  • Não trate valid=True como “o FHIR está certo”. Pra conteúdo, passe o recurso por um validador de verdade, como o validador oficial da HL7 ou o $validate do seu servidor, e use o perfil que o seu caso exige. Eu não rodei nenhum dos dois nesta demo.
  • Pra CPF, valide o dígito fora do FHIR. São poucas linhas, e não tratam sequência repetida (11111111111) nem CPF com máscara:
def digito_cpf(base):
    soma = sum(d * p for d, p in zip(base, range(len(base) + 1, 1, -1)))
    resto = (soma * 10) % 11
    return 0 if resto == 10 else resto

def cpf_confere(cpf):
    d = [int(c) for c in cpf]
    return len(d) == 11 and digito_cpf(d[:9]) == d[9] and digito_cpf(d[:10]) == d[10]

Limites

  • O gabarito é meu. Escolhi as 9 categorias e fiz o defeito de cada uma. Com outras categorias a proporção seria outra. O 80 de 180 descreve este conjunto, e não a taxa de erro de FHIR real.
  • Não comparei com o validador oficial da HL7. A demo diz o que o validate_resource() deixa passar, não o que outro validador pegaria.
  • Perfil brasileiro não entra. Usei o sistema de identificador de CPF do Ministério da Saúde só pra montar o dado.
  • Testei só validate_resource(), em recurso solto e em Bundle montado à mão. Não testei load_bundle() nem o resto do SDK (gateway, MCP, sandbox).
  • 240 recursos sintéticos, uma semente, FHIR R4B, três tipos de recurso e Bundle. O 0 de 60 falsos positivos vale pro controle que eu mesmo gerei.

Fonte e como reproduzir

Clone https://github.com/rogeriorrodrigues/nursia-research-lab e entre em demos/11-healthchain-validacao-fhir. Daí são três comandos:

pip install -r requirements.txt
python3 demo.py
python3 -m pytest tests -q

NursIA é pesquisa do PPGINFOS/UFSC, com bolsa FAPESC.

In English

HealthChain 0.16.1 validate_resource() rejected 80 of 180 defective synthetic FHIR resources: all 80 structural defects and none of the 100 content defects (invented LOINC code, non-UCUM unit, broken reference, CPF with a bad check digit, invariant obs-6). No false positives on 60 valid resources. The module’s docstring already lists most of these as unchecked; the demo measures the gap.

Conhece alguém que precisa ler isso?

Manda pra essa pessoa. Leva dez segundos e é o que mais ajuda esse trabalho a chegar em quem usa IA com dado de verdade.

Rogério Rodrigues

Rogério Rodrigues

Engenheiro de IA há 20 anos em software, mestrando em Informática em Saúde na UFSC. Escrevo o que rodei: IA com dado de saúde, FHIR e LGPD, com código aberto.

Ficou uma dúvida? Discorda de algo? Me chama.

Ver publicações científicas