Pular para conteúdo

0019 — Bibliotecas compartilhadas da casa

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12 · Decidido em: 2026-08-04

Contexto

O monorepo X-Adm propaga infra por copy-paste, não por biblioteca. A auditoria de 2026-08-03 (grafo graphify-out/ + leitura dos repos) achou 28 nomes de classe idênticos em 3–4 clientes — a stack de auth inteira, RFC-7807, health, versão, Sentry — mais PowerSync-admin (~18) e storage (~6) idênticos entre onpetro e vantroba. Só 1,9% das arestas do grafo cruzam fronteira de app: não há espinha dorsal de código compartilhado, só N cópias que envelhecem em paralelo.

O custo é dobrado. Manutenção: corrigir um bug de auth é editar a mão em N repos, e cada um pode ficar para trás. Segurança: a stack de auth copiada casa com um IdP Firebase compartilhado (xadm-6ab81) — um drift no check de domínio de um app vira exposição cross-client, e o drift é invisível porque não há fonte única a conferir.

A constituição prescreve comum/core dentro de cada app (package-by-feature, Java/Micronaut), mas não tem mecanismo de biblioteca compartilhada entre apps. Sem ele, "não copie infra" é regra sem máquina — e regra sem máquina é o que já falhou.

Decisão

Infra idêntica cross-app vive num único repo xadm-commons, cada lib é um módulo Gradle com SemVer próprio, publicada como artefato Maven no Forgejo Packages; o app consome por versão, não por cópia.

  • Um repo multi-módulo (xadm-commons), não um repo por lib. Cada capacidade transversal é um módulo Gradle (xadm-seguranca, xadm-comum-web, xadm-comum-storage…) com seu gradle.properties/version independente — versionar a auth não obriga bumpar o storage. Um repo, um pipeline, N artefatos.
  • Base de pacote/group br.com.xadm — decidido, é o group Maven e a raiz de pacote de toda lib (br.com.xadm.comum.*, br.com.xadm.seguranca.*). O int-sascar, que hoje usa biz.xadm, migra para br.com.xadm ao adotar a primeira lib (o compilador é o gate do rename). Nada nasce em biz.xadm.
  • Artefato Maven, registro público (fonte.xadm.biz). O CI do xadm-commons publica cada módulo (publishToMavenRepository) no registry de pacotes do Forgejo da org. O app consome declarando o repositório e a dependência por versão fixa:

Correção 2026-08-10: o registro passou de Forgejo Packages privado para público em fonte.xadm.biz — leitura anônima, sem credencial; publicar ainda pede FORGEJO_PACKAGES_* (só o CI do xadm-commons).

repositories {
    mavenCentral()
    // Registro PÚBLICO — leitura anônima, sem credentials{} (a URL não é segredo).
    maven { url = uri("https://fonte.xadm.biz/api/packages/xadm/maven") }
}
dependencies {
    implementation("br.com.xadm:xadm-seguranca:0.2.0")   // versão fixa
}

Ler não pede credencial (registro público); publicar sim — o CI do xadm-commons usa FORGEJO_PACKAGES_USER/TOKEN (secret da org no Forgejo / env do Coolify), nunca hard-coded no build.gradle.kts nem em .ia/, como todo segredo da casa (Segurança §Segredos). A assimetria publish×read é o que mudou.

  • Set publicado — oito módulos no ar em https://fonte.xadm.biz/xadm/-/packages, group br.com.xadm: xadm-seguranca, xadm-comum-web, xadm-mensageria, xadm-comum-util, xadm-comum-storage, xadm-comum-teste, xadm-comum-powersync, xadm-ingestion-core.

  • Versão corrente, piso e estado de entrega não moram nesta decisão.

Correção 2026-09-12: a versão corrente vem do registro; o piso, com o motivo, e a migração de cada versão vêm do CHANGELOG do módulo — ver Bibliotecas da casa. A tabela de versões e o registro de entregas por módulo que viviam aqui saíram: envelheciam entre releases da lib. A política de acompanhar a versão corrente é a 0034.

  • App consome por versão publicada, não por fonte local. Nada de includeBuild/composite apontando o checkout do xadm-commons como norma — o app compila contra o artefato publicado, que é o que a produção embarca (mesmo princípio "gate fiel ao artefato", constituição §6). Composite build fica para desenvolvimento local pontual da lib.
  • Extração é conservadora e caracterizada. Só sobe para a lib o que for byte-idêntico entre apps (a menos de CRLF e nome de pacote); classe divergente exige decisão explícita de reconciliação antes de subir — não se unifica corpo diferente às cegas. Cada fase de extração produz a matriz idêntico/divergente das cópias como primeiro work-item.
  • A extração não pode REGREDIR segurança nem robustez — reconcilie pro baseline endurecido, não pro mais simples. Byte-idêntico é gatilho de revisão, não meta: copiar uma classe fielmente também copia os defeitos da origem — e uma vez na lib, o defeito alcança todo adotante transitivo. Regra: (1) se um app de origem tinha endurecimento (comparação constant-time, guard de vazio/fail-closed, default de config que não quebra boot), a lib porta o endurecimento; (2) se as cópias divergiam, canoniza-se a mais forte em segurança/robustez, não a mais curta. Toda classe que toca segredo/auth roda o checklist do revisor na extração — diff semântico contra a origem, não textual. Incidente que originou a regra: o StaticBearerTokenValidator subiu byte-idêntico e chegou na lib abaixo do piso da casa (String.equals timing-vulnerável, sem guard de vazio → auth-bypass, @Value sem default → boot quebrado pra quem nem usa a feature), forçando o adotante a re-endurecer via @Replaces — a extração não cumpriu o objetivo (parar de forkar). Piso de segredo/auth em Segurança §Authn/authz.
  • Ordem de extração (cada uma sub-lineage no repo-alvo, citando raiz/001): mecanismo (esta decisão) → segurança (xadm-seguranca, risco alto: centraliza o validador do IdP compartilhado, audience/domínio segue config por-app) → web (xadm-comum-web: RFC-7807, health, versão, request-id/MDC — ver contrato app↔app, 0020) → contrato de comms → registry anti-god-node → ingestão/storage. A extração em si não é escopo desta decisão; aqui fica como a lib existe.

Alternativas descartadas

  • Um repo por lib (xadm-seguranca, xadm-commons-web… cada um seu repo). N pipelines de release, N tags a coordenar, mais cerimônia para a mesma coisa. O multi-módulo dá SemVer independente por módulo sem multiplicar repositórios.
  • Git submodule. A "versão" de um submódulo é um SHA, não SemVer — acopla a fonte (o app carrega a árvore da lib), não o artefato, e some do radar de "que versão estou usando". Bump vira git cerimonioso em vez de mudar uma linha de dependencies.
  • Continuar com copy-paste + disciplina. É exatamente o que produziu 28 classes duplicadas e o risco de drift do IdP. Regra sem máquina não segura.

Consequências

  • Fim do copy-paste de infra. Corrigir um bug de auth passa a ser um bump de versão que todos os apps recebem no próximo implementation(...) — não N edições à mão. O drift do IdP compartilhado cai (um validador, não N cópias); audience/domínio continua config por-app.
  • xadm-commons ganha um dono. Alguém responde pelo repo, pela política de versão e pela fronteira de cada módulo (ArchUnit no gate: módulo não depende de app; comum não depende de feature). Sem dono, a lib vira mais um lugar que envelhece.
  • Toda adoção é um passo coordenado por repo. Adotar xadm-seguranca no onpetro/thoms migra o pacote security→seguranca (vantroba já é seguranca) — o compilador acusa o rename incompleto. É lift-and-shift + testes de paridade, não redesign da semântica de auth.
  • Precisa da coordenada do registro. URL e credencial do Forgejo Packages são pré-condição operacional (via <VAR>), levantadas fora deste ADR.
  • Reversível por módulo. Um app pode pinar uma versão antiga enquanto migra; a lib mantém compat na janela de deprecação (mesma disciplina do contrato, 0020).
  • <svc>-api (contrato app↔app) reusa este mecanismo, mas NÃO é xadm-commons. O módulo de contrato tipado que um receptor publica (0020) usa o mesmo transporte (Maven no Forgejo, group br.com.xadm, consumo por versão), porém é artefato do app receptor, publicado do próprio repo — não entra no xadm-commons, que é infra transversal (auth, web, storage…), não o contrato de um app específico.