Ir para o conteúdo

API de plugins de mapeamento

Referência técnica completa do sistema de plugins de mapeamento do Anotae. Descreve o formato JSON dos arquivos de mapeamento, as classes e funções Python públicas, o processo de descoberta automática e os comandos CLI relacionados.


1. Visão geral

O sistema de mapeamento define como campos de um atendimento Anotae (modelo SOAP + vocabulários clínicos) são injetados nos formulários de sistemas externos como e-SUS APS PEC e SISREG III. Cada mapeamento é um arquivo JSON puro — sem código executável — interpretado pela engine de injeção na extensão de navegador.

Três fontes de mapeamentos são suportadas, em ordem crescente de prioridade:

Fonte Localização Prioridade
Built-in vocabularies/mappings/ (dentro do pacote instalado) Menor
User-level ~/.anotae/mappings/ Média
Custom Caminhos em ANOTAE_MAPPING_PATHS (variável de ambiente) Maior

Quando dois arquivos definem o mesmo id, a fonte de maior prioridade vence e a fonte substituída é registrada no DiscoveryReport.overridden. Mapeamentos inválidos são pulados com aviso estruturado — nunca derrubam o app.


2. Estrutura de um mapping JSON

2.1 Campos do objeto raiz

Campo Tipo Obrigatório Descrição
id string Sim Identificador único do mapeamento (slug sem espaços)
name string Sim Nome legível, exibido na UI e no CLI
target_system string Sim Sistema-alvo (ex.: "esus-aps-pec", "sisreg-iii")
target_version string Sim Versão do sistema-alvo testada (ex.: "5.x")
version string Sim Versão semântica do arquivo de mapeamento (ex.: "2.0.0")
description string Não Descrição para fins de auditoria e documentação
fields array Sim Lista de mapeamentos de campo (ver §2.2)
_unmapped_fields array Não Campos do sistema-alvo sem correspondência — documentação informativa, ignorado pela engine

2.2 Estrutura de um item de fields

Cada elemento do array fields é um objeto com os campos abaixo.

Campo Tipo Obrigatório Descrição
source string Sim Caminho do campo no modelo Anotae (ex.: "soap.subjective", "vitals.weight_kg")
target_selectors array Sim Lista de seletores DOM tentados em ordem até o primeiro que encontrar o elemento (ver §2.3)
required boolean Não (padrão false) Se true, a engine registra erro quando o campo não é encontrado no DOM
transform string \| null Não Transformação pré-injeção (ex.: "first_code" para extrair primeiro código de uma lista)

2.3 Estrutura de um item de target_selectors

Campo Tipo Valores válidos Descrição
type string "id", "name", "label", "aria-label", "data-testid", "css" Estratégia de localização do elemento DOM
value string Valor do seletor para a estratégia escolhida

A engine tenta cada seletor na ordem declarada e usa o primeiro que localizar o elemento. Isso permite robustez frente a variações de versão do sistema-alvo.

2.4 Exemplo completo

Mapeamento built-in para o Atendimento Individual do e-SUS APS PEC (vocabularies/mappings/esus-aps-pec-atendimento-individual.json):

{
  "id": "esus-aps-pec-atendimento-individual",
  "name": "e-SUS APS PEC - Atendimento Individual",
  "target_system": "esus-aps-pec",
  "target_version": "5.x",
  "version": "2.0.0",
  "description": "Mapeamento de campos Anotae para o formulário de Atendimento Individual do e-SUS APS PEC.",
  "fields": [
    {
      "source": "soap.subjective",
      "target_selectors": [
        {"type": "id",         "value": "subjetivo"},
        {"type": "aria-label", "value": "Subjetivo"},
        {"type": "label",      "value": "Subjetivo"},
        {"type": "css",        "value": "textarea[placeholder*='Subjetivo']"}
      ],
      "required": false,
      "transform": null
    },
    {
      "source": "ciap2_codes",
      "target_selectors": [
        {"type": "data-testid", "value": "subjetivoCiap"},
        {"type": "name",        "value": "ciap"}
      ],
      "required": false,
      "transform": "first_code"
    },
    {
      "source": "vitals.weight_kg",
      "target_selectors": [
        {"type": "name", "value": "objetivo.medicoes.peso"}
      ],
      "required": false,
      "transform": null
    }
  ],
  "_unmapped_fields": [
    {
      "target_name": "finalizacao.tipoAtendimento",
      "reason": "Depende de contexto de agendamento, não disponível no Anotae v0.x"
    }
  ]
}

2.5 Valores documentados de source

Os caminhos de source reconhecidos pela engine de injeção:

source Descrição
soap.subjective Seção S do SOAP
soap.objective Seção O do SOAP
soap.assessment Seção A do SOAP
soap.plan Seção P do SOAP
ciap2_codes Lista de códigos CIAP-2 selecionados
cid10_codes Lista de códigos CID-10 selecionados
sigtap.procedure_code Código SIGTAP do procedimento
vitals.weight_kg Peso em kg
vitals.height_cm Altura em cm
vitals.systolic_bp PA sistólica (mmHg)
vitals.diastolic_bp PA diastólica (mmHg)
vitals.heart_rate Frequência cardíaca (bpm)
vitals.respiratory_rate Frequência respiratória (ipm)
vitals.temperature_celsius Temperatura (°C)
vitals.oxygen_saturation Saturação de O₂ (%)
vitals.blood_glucose Glicemia (mg/dL)
vitals.waist_circumference_cm Circunferência abdominal (cm)
vitals.head_circumference_cm Perímetro cefálico (cm)
vitals.calf_circumference_cm Perímetro da panturrilha (cm)

3. API Python

Todos os símbolos públicos estão em anotae.core.mapping_plugin e anotae.core.mapping_engine.

3.1 MappingSource — enum

# anotae/core/mapping_plugin.py
class MappingSource(StrEnum):
    BUILTIN = "builtin"
    USER    = "user"
    CUSTOM  = "custom"

Representa a origem de um plugin descoberto. Valores com prioridade crescente: BUILTIN (0) < USER (1) < CUSTOM (2).

3.2 SelectorDef — dataclass

# anotae/core/mapping_engine.py
@dataclass(frozen=True)
class SelectorDef:
    type: str   # "id" | "name" | "label" | "aria-label" | "data-testid" | "css"
    value: str

3.3 FieldMappingDef — dataclass

@dataclass(frozen=True)
class FieldMappingDef:
    source: str
    target_selectors: list[SelectorDef]  # default: []
    required: bool                        # default: False
    transform: str | None                 # default: None

3.4 MappingDefinition — dataclass

Representa um arquivo de mapeamento completamente parseado.

@dataclass(frozen=True)
class MappingDefinition:
    id:             str
    name:           str
    target_system:  str
    target_version: str
    version:        str
    description:    str                  # default: ""
    fields:         list[FieldMappingDef]  # default: []

3.5 MappingPlugin — dataclass

Mapeamento descoberto associado à sua origem no sistema de arquivos.

# anotae/core/mapping_plugin.py
@dataclass(frozen=True)
class MappingPlugin:
    definition: MappingDefinition
    source:     MappingSource
    path:       Path

3.6 DiscoveryReport — dataclass

Resultado completo de uma execução de discover().

@dataclass(frozen=True)
class DiscoveryReport:
    plugins:    dict[str, MappingPlugin]   # id → plugin ativo
    skipped:    list[tuple[Path, str]]     # (caminho, motivo do erro)
    overridden: list[str]                  # ids substituídos por prioridade

3.7 discover() -> DiscoveryReport

from anotae.core.mapping_plugin import discover

report = discover()

Varre as três fontes (built-in, user-level, custom) na ordem correta, resolve conflitos por prioridade e retorna um DiscoveryReport. Chamada idempotente — cada invocação relê o disco.

3.8 load_mapping(path: Path) -> MappingDefinition

from anotae.core.mapping_engine import load_mapping

definition = load_mapping(Path("~/.anotae/mappings/meu-mapping.json"))

Carrega e parseia um único arquivo JSON. Lança ValidationError se o arquivo não existir, não for JSON válido ou não puder ser parseado.

3.9 validate_mapping(mapping: MappingDefinition) -> list[str]

from anotae.core.mapping_engine import validate_mapping

erros = validate_mapping(definition)
if erros:
    print("Mapping inválido:", erros)

Retorna lista de mensagens de erro de coerência (campos obrigatórios ausentes, tipos de seletor inválidos etc.). Lista vazia indica mapping válido.

3.10 list_by_target_system(target_system, *, report=None) -> list[MappingPlugin]

from anotae.core.mapping_plugin import list_by_target_system

plugins = list_by_target_system("esus-aps-pec")

Filtra plugins por target_system. Se report não for passado, chama discover() internamente. Retorna lista ordenada por id.

3.11 MappingRegistry — classe

Registry injetável para testes e injeção de dependência.

from anotae.core.mapping_plugin import MappingRegistry

# A partir da descoberta real
registry = MappingRegistry.from_discovery()

# Ou com conjunto controlado (testes)
registry = MappingRegistry(plugins={})
Método Assinatura Descrição
from_discovery() cls -> MappingRegistry Cria instância com todos os plugins descobertos
get() (mapping_id: str) -> MappingPlugin \| None Busca plugin por id
all() () -> list[MappingPlugin] Todos os plugins ordenados por id
by_target_system() (target_system: str) -> list[MappingPlugin] Filtra por sistema-alvo
register() (plugin: MappingPlugin) -> None Registra ou substitui plugin
unregister() (mapping_id: str) -> bool Remove plugin; retorna True se existia

4. Descoberta automática

4.1 Ordem de varredura

discover() varre as fontes nesta sequência fixa:

  1. vocabularies/mappings/ (caminho relativo ao pacote instalado)
  2. ~/.anotae/mappings/
  3. Cada caminho em ANOTAE_MAPPING_PATHS, na ordem declarada

Arquivos schema.json são ignorados em todos os diretórios.

4.2 Resolução de conflitos

Quando dois arquivos têm o mesmo id:

  • A fonte de maior prioridade (CUSTOM > USER > BUILTIN) vence.
  • O id substituído é adicionado a DiscoveryReport.overridden.
  • Fontes de mesma prioridade em caminhos diferentes: o último caminho declarado em ANOTAE_MAPPING_PATHS vence (varredura sequencial).

4.3 Variável de ambiente ANOTAE_MAPPING_PATHS

Formato: caminhos absolutos separados pelo separador do sistema operacional (os.pathsep: no Linux/macOS, ; no Windows).

# Linux / macOS
export ANOTAE_MAPPING_PATHS="/opt/mappings-institucional:/tmp/mappings-dev"

# Windows
set ANOTAE_MAPPING_PATHS=C:\mappings-institucional;C:\mappings-dev

Caminhos inexistentes são silenciosamente ignorados (não causam erro).

4.4 Tratamento de erros de validação

Cada arquivo com problema é adicionado a DiscoveryReport.skipped como (Path, motivo_str). Motivos possíveis:

  • ValidationError: ... — JSON inválido ou campo obrigatório ausente
  • I/O: ... — erro de leitura do arquivo (permissão, encoding)
  • errors: ... — falha nas regras de coerência de validate_mapping()

O app continua normalmente; apenas os arquivos problemáticos são omitidos.


5. CLI

anotae mappings list

Lista todos os mapeamentos descobertos.

anotae mappings list

Filtra por sistema-alvo com --target-system:

anotae mappings list --target-system esus-aps-pec

Exemplo de saída:

ID                                        NOME                                    SISTEMA          VERSÃO  FONTE
esus-aps-pec-atendimento-individual       e-SUS APS PEC - Atendimento Individual  esus-aps-pec     2.0.0   builtin
sei-relatorio-ses-df                      SEI SES-DF - Relatório de Atendimento   sei              1.0.0   user
sisreg-iii-solicitacao                    SISREG III - Solicitação                sisreg-iii       1.1.0   builtin

A coluna FONTE indica a origem: builtin, user ou custom.


6. Criar um mapping personalizado

Guia rápido para criar um mapeamento user-level em ~/.anotae/mappings/.

Passo 1 — Criar o diretório (se não existir)

mkdir -p ~/.anotae/mappings

Passo 2 — Criar o arquivo JSON

# Exemplo: ~/.anotae/mappings/meu-sistema-local.json
{
  "id": "meu-sistema-local",
  "name": "Meu Sistema Local - Formulário de Consulta",
  "target_system": "meu-sistema-local",
  "target_version": "1.0",
  "version": "1.0.0",
  "description": "Mapeamento privado para sistema interno da unidade.",
  "fields": [
    {
      "source": "soap.subjective",
      "target_selectors": [
        {"type": "id",   "value": "campoQueixaPrincipal"},
        {"type": "name", "value": "queixa"}
      ],
      "required": false,
      "transform": null
    },
    {
      "source": "soap.plan",
      "target_selectors": [
        {"type": "css", "value": "textarea.plano-terapeutico"}
      ],
      "required": false,
      "transform": null
    }
  ]
}

Passo 3 — Verificar com o CLI

anotae mappings list

O novo mapping deve aparecer na coluna FONTE como user.

Passo 4 — Inspecionar erros (se o mapping não aparecer)

from anotae.core.mapping_plugin import discover

report = discover()
for path, motivo in report.skipped:
    print(f"Pulado: {path}{motivo}")

Passo 5 — Testar injeção

Abra o sistema-alvo no navegador com a extensão Anotae ativa, preencha um atendimento e acione a injeção. Verifique no painel da extensão se os campos foram mapeados corretamente.

Nota: mappings user-level têm prioridade sobre built-ins. Se você criar um arquivo com o mesmo id de um mapeamento built-in, o seu substituirá o padrão sem alterar o pacote instalado.


7. Segurança e sandboxing

Dados, nunca código

Os arquivos de mapeamento são exclusivamente dados (JSON). Não há suporte a expressões arbitrárias, scripts, lambdas ou qualquer forma de código executável nos arquivos de mapeamento. A engine de injeção na extensão interpreta os seletores DOM e os valores de source — não há eval() nem execução dinâmica em nenhum ponto do pipeline.

Validação de seletores na carga

validate_mapping() verifica que cada type em target_selectors pertence ao conjunto de estratégias reconhecidas: id, name, label, aria-label, data-testid, css. Seletores com tipo inválido causam rejeição do arquivo inteiro com aviso em DiscoveryReport.skipped.

Sem acesso à rede

Mappings não têm mecanismo para fazer requisições de rede. Nenhum campo do schema aceita URLs, callbacks ou referências externas. A engine de injeção opera inteiramente no DOM local da aba do navegador.

Integridade dos built-ins

Os mapeamentos built-in em vocabularies/mappings/ fazem parte do pacote distribuído, cobertos pela attestation Sigstore gerada em cada release (.github/workflows/build-windows.yml). Mapeamentos user-level e custom não passam por essa verificação — responsabilidade do operador.


Gerado em 2026-05-19. Fonte de verdade: anotae/core/mapping_plugin.py e anotae/core/mapping_engine.py.