O modelo
06 · Modelo zerado e lacunas
O modelo zerado é um repositório que sabe tudo sobre como trabalhamos e nada sobre a tecnologia do produto. Onde falta essa informação, há uma lacuna marcada. A primeira ação de uma stack é preencher as lacunas — e o modelo impede que o desenvolvimento comece antes disso.
O modelo mora neste repositório, na pasta modelo/, versionado junto com as regras e as ORGs
(ADR-0002). As ORGs não ficam
dentro dele: o eng/new-stack.sh as copia de docs/decisoes/organizacao/ ao criar a stack e
escreve o arquivo ARQUITETURA com a versão.
O que é uma lacuna
Uma lacuna é um marcador procurável num arquivo do modelo:
{{LACUNA:<id>}}<id>é estável e único (stack.linguagem,persistencia.banco,app.lista…), para que a mesma lacuna em vários arquivos seja preenchida com o mesmo valor;- cada
idtem a sua descrição num lugar só: o catálogo das lacunas, no fim do roteiro da Fase 1 (docs/guias/definir-a-stack.md), com o que preencher e se é de escolha ou de valor. O modelo de hoje tem 93; - o formato é ASCII de propósito:
grep -rn '{{LACUNA:' .lista todas; eng/gaps.shfaz essa busca e falha se encontrar alguma. Ele roda na CI desde o primeiro commit — enquanto houver lacuna, a CI fica vermelha, e isso é o esperado na Fase 1.
Dois tipos de lacuna
| Tipo | O que é | Exemplo | Onde mora |
|---|---|---|---|
| De escolha | O que é: Uma decisão com alternativas | Exemplo: "linguagem e runtime", "banco de dados" | Onde mora: ficha + ADR pré-escrita correspondente (contexto e alternativas) |
| De valor | O que é: Um dado que decorre de escolhas já feitas | Exemplo: "porta local da aplicação", "prefixo no Bitwarden", "nome do realm" | Onde mora: ficha + arquivos que usam o valor |
Lacunas de escolha sempre viram texto de ADR (com alternativas consideradas); lacunas de valor só precisam do valor.
A ficha da stack (docs/stack.md)
A ficha é o único lugar onde a stack declara sua tecnologia. É curta, tabelada e lida em segundos. As ADRs explicam o porquê; a ficha diz o quê.
Abaixo, a ficha zerada como ela vem no modelo. Ao lado de cada lacuna, como ficaria no produto fictício dos exemplos, o atendimento (03), para dar o tom.
1. Produto e aplicações
| Campo | Lacuna | Exemplo |
|---|---|---|
| Nome do produto | Lacuna: {{LACUNA:produto.nome}} | Exemplo: atendimento |
| Plataforma | Lacuna: {{LACUNA:produto.plataforma}} | Exemplo: Enterprise |
| Repositório | Lacuna: {{LACUNA:produto.repositorio}} | Exemplo: enterprise-stack-atendimento |
| Propósito em uma frase | Lacuna: {{LACUNA:produto.proposito}} | Exemplo: atendimento a clientes com créditos pré-pagos |
| Aplicações | Lacuna: {{LACUNA:app.lista}} | Exemplo: ver tabela abaixo |
| Aplicação | Público | Tipo | Acesso | Endereço | Realm | Família de cor | Porta local |
|---|---|---|---|---|---|---|---|
{{LACUNA:app.nome}} | Público: {{LACUNA:app.publico}} | Tipo: {{LACUNA:app.tipo}}: web com backend · site estático | Acesso: {{LACUNA:app.acesso}}: restrita · pública · mista | Endereço: {{LACUNA:app.endereco}} | Realm: {{LACUNA:app.realm}} | Família de cor: {{LACUNA:app.familia}} | Porta local: {{LACUNA:app.porta}} |
ex. atendimento-gestao | Público: colaboradores | Tipo: web com backend | Acesso: restrita | Endereço: gestao-atendimento.enterprise.hibex.net | Realm: hibex-team | Família de cor: Oceano | Porta local: 8081 |
ex. atendimento-cliente | Público: clientes externos | Tipo: web com backend | Acesso: restrita | Endereço: atendimento.hibex.app | Realm: hibex-enterprise | Família de cor: Aurora | Porta local: 8080 |
ex. atendimento-site | Público: visitantes | Tipo: site estático | Acesso: pública | Endereço: atendimento.hibex.co | Realm: — | Família de cor: Menta | Porta local: 8070 |
2. Tecnologia das aplicações
| Campo | Lacuna | Exemplo | Fonte única da versão |
|---|---|---|---|
| Linguagem + versão | Lacuna: {{LACUNA:stack.linguagem}} | Exemplo: Java 21 (LTS) | Fonte única da versão: {{LACUNA:stack.fonte-versao-linguagem}} (ex.: toolchain no build) |
| Framework de aplicação + versão | Lacuna: {{LACUNA:stack.framework}} | Exemplo: Spring Boot 4.1.1 | Fonte única da versão: {{LACUNA:stack.fonte-versao-dependencias}} (ex.: catálogo de versões) |
| Abordagem de UI | Lacuna: {{LACUNA:stack.ui}} | Exemplo: renderizada no servidor (Thymeleaf + htmx 2.0.x) | Fonte única da versão: {{LACUNA:stack.fonte-versao-dependencias}} |
| Cliente OIDC | Lacuna: {{LACUNA:stack.oidc}} | Exemplo: Spring Security OAuth2 Client | Fonte única da versão: {{LACUNA:stack.fonte-versao-dependencias}} (ex.: BOM do framework) |
| Ferramenta de build + versão | Lacuna: {{LACUNA:stack.build}} | Exemplo: Gradle 8.14.3 (Kotlin DSL, wrapper) | Fonte única da versão: {{LACUNA:stack.fonte-versao-build}} (ex.: wrapper versionado) |
| Gerenciador de dependências | Lacuna: {{LACUNA:stack.dependencias}} | Exemplo: version catalog gradle/libs.versions.toml | Fonte única da versão: — |
| Estrutura de módulos | Lacuna: {{LACUNA:stack.modulos}} | Exemplo: core (domínio, JPA, migrations) + um módulo por aplicação | Fonte única da versão: — |
3. Persistência
O modelo não tem banco padrão: cada stack escolhe os seus armazenamentos (banco relacional, MongoDB, fila, armazenamento de arquivos, cache…) numa ADR, pelos critérios abaixo. O PostgreSQL é a opção já provada para dados relacionais, não uma imposição.
| Critério | Pergunta |
|---|---|
| Backup | Pergunta: Há ferramenta de backup e uma restauração que se pode testar localmente? |
| Imagem | Pergunta: Há imagem oficial com tag completa, para o Compose, os testes e o deploy? |
| Testes | Pergunta: Sobe em container nos testes de integração e no E2E, e se zera rápido? |
| Evolução | Pergunta: Como o formato dos dados muda com o produto (migrations ou equivalente), e quem é o dono? |
| Operação | Pergunta: O toolbox já sabe observar e guardar isso, ou é coisa nova para a plataforma? |
| Campo | Lacuna | Exemplo | Fonte única da versão |
|---|---|---|---|
| Armazenamentos + versões | Lacuna: {{LACUNA:persistencia.banco}} | Exemplo: PostgreSQL 16.x (ou MongoDB, uma fila, um bucket…) | Fonte única da versão: compose.yaml + deploy.yml (mesma tag) |
| Acesso a dados | Lacuna: {{LACUNA:persistencia.acesso}} | Exemplo: Spring Data JPA / Hibernate | Fonte única da versão: BOM |
| Migrations | Lacuna: {{LACUNA:persistencia.migrations}} | Exemplo: Flyway, em core/src/main/resources/db/migration | Fonte única da versão: BOM |
| Dono do schema | Lacuna: {{LACUNA:persistencia.dono}} | Exemplo: a aplicação de gestão migra no boot; as outras validam | Fonte única da versão: — |
| Bancos por aplicação | Lacuna: {{LACUNA:persistencia.topologia}} | Exemplo: um banco compartilhado | Fonte única da versão: — |
| Backup | Lacuna: {{LACUNA:persistencia.backup}}; sem backup: {{LACUNA:persistencia.sem-backup}} | Exemplo: prodrigestivill/postgres-backup-local:16-debian-<hash>; sem backup: nenhum | Fonte única da versão: deploy.yml |
4. Testes
Em todas as camadas, o teste vem antes do código (TDD, ORG-0009).
| Campo | Lacuna | Exemplo | Fixo pela arquitetura |
|---|---|---|---|
| Unidade | Lacuna: {{LACUNA:testes.unidade}} | Exemplo: JUnit 5 + AssertJ | Fixo pela arquitetura: sem rede, sem container |
| Integração | Lacuna: {{LACUNA:testes.integracao}} | Exemplo: Spring Boot Test + Testcontainers 2.x | Fixo pela arquitetura: dependências reais em container |
| Integração com identidade | Lacuna: {{LACUNA:testes.identidade}} | Exemplo: Keycloak em Testcontainers (GenericContainer) | Fixo pela arquitetura: Keycloak real, na imagem do contrato da plataforma |
| Fim a fim | Lacuna: — | Exemplo: — | Fixo pela arquitetura: Playwright {{LACUNA:e2e.versao-playwright}} + Node {{LACUNA:e2e.versao-node}}, TypeScript, Chromium do Playwright, harness hierárquico |
| Ambiente do E2E | Lacuna: {{LACUNA:e2e.ambiente}} | Exemplo: PostgreSQL zerado por psql; e-mail num servidor SMTP de captura | Fixo pela arquitetura: a interface dos preparadores e o perfil e2e do Compose (doc 11) |
| Comando que roda tudo | Lacuna: {{LACUNA:testes.comando}} | Exemplo: ./gradlew build | Fixo pela arquitetura: um comando só |
5. Containers e implantação
| Campo | Lacuna | Exemplo |
|---|---|---|
| Imagem de build | Lacuna: {{LACUNA:container.imagem-build}} | Exemplo: gradle:8.14.3-jdk21 |
| Imagem de runtime | Lacuna: {{LACUNA:container.imagem-runtime}} | Exemplo: eclipse-temurin:21.0.x_y-jre |
| Site estático | Lacuna: {{LACUNA:container.imagem-estatico}} | Exemplo: nginx:1.x.y-alpine |
| Host(s) da VPS | Lacuna: {{LACUNA:deploy.hosts}} | Exemplo: web.ovhcloudlab.hibex.net |
| Registro de imagens | Lacuna: — (fixo: ghcr.io) | Exemplo: ghcr.io/hibex-solutions/<app> |
| Prefixo no Bitwarden | Lacuna: {{LACUNA:deploy.prefixo-segredos}} (um projeto para o produto) | Exemplo: ATENDIMENTO_ |
6. Identidade
| Campo | Lacuna | Exemplo |
|---|---|---|
| Realms usados | Lacuna: {{LACUNA:identidade.realms}} | Exemplo: hibex-team (colaboradores), hibex-enterprise (clientes) |
Realms novos a criar no toolbox | Lacuna: {{LACUNA:identidade.realms-novos}} | Exemplo: hibex-enterprise |
| Permissões e perfis | Lacuna: {{LACUNA:identidade.acesso}} | Exemplo: em docs/acesso/<app>.md |
| IdPs externos | Lacuna: {{LACUNA:identidade.idps}} | Exemplo: Google, um OAuth client por realm |
| Keycloak | Lacuna: — (fixo: o contrato da plataforma) | Exemplo: quay.io/keycloak/keycloak:26.7.1 |
7. Observabilidade
| Campo | Lacuna | Exemplo | Fixo pela arquitetura |
|---|---|---|---|
| Log estruturado | Lacuna: {{LACUNA:obs.log}} | Exemplo: estruturado nativo do Spring Boot (ECS) | Fixo pela arquitetura: JSON no stdout, uma linha por evento |
| Métricas | Lacuna: {{LACUNA:obs.metricas}} | Exemplo: Micrometer + registry Prometheus | Fixo pela arquitetura: Prometheus em porta interna |
| Traces | Lacuna: {{LACUNA:obs.traces}} | Exemplo: Micrometer Tracing + bridge OTel + exporter OTLP | Fixo pela arquitetura: OTLP para o Alloy |
| Porta interna | Lacuna: {{LACUNA:obs.porta}} | Exemplo: 9090 | Fixo pela arquitetura: não publicada |
8. Pré-requisitos de máquina
| Ferramenta | Versão | Para quê |
|---|---|---|
| Linux | Versão: — | Para quê: plataforma (fixo) |
| Docker Engine + Compose v2 | Versão: {{LACUNA:prereq.docker}} | Para quê: ambiente local, testes, imagens (fixo) |
| Node.js + npm | Versão: {{LACUNA:prereq.node}} | Para quê: OpenSpec CLI, E2E, verificador do design system (fixo) |
| OpenSpec CLI | Versão: — (o modelo traz a versão; a stack atualiza por change) | Para quê: changes (fixo) |
| Git + Git LFS | Versão: {{LACUNA:prereq.git}} | Para quê: (LFS só se a stack mantiver acervo de capturas: {{LACUNA:acervo.manter}}, ADR-0011) |
| Python 3 | Versão: o do sistema, 3.10 ou mais novo | Para quê: hooks do agente, eng/pinned-versions.sh (fixo) |
| Kamal | Versão: {{LACUNA:prereq.kamal}} | Para quê: deploy — só o mantenedor (fixo) |
Bitwarden SM CLI (bws) | Versão: {{LACUNA:prereq.bws}} | Para quê: segredos — só o mantenedor (fixo) |
{{LACUNA:prereq.runtime}} | Versão: | Para quê: runtime da linguagem, se roda nativo |
Fonte única de versão
Uma versão escrita em dois lugares diverge. Por isso a ficha diz onde cada versão mora, e todos os outros lugares a referenciam ou a conferem.
eng/pinned-versions.sh (modelo) falha quando encontra:
latest,*,^x,~x,>=x,1.+,latest.release, faixas Maven e-SNAPSHOTnos manifestos que conhece:package.json,requirements*.txt,*.versions.toml,build.gradle(.kts),pom.xml,.nvmrc,.node-version,.python-version,.tool-versionseeng/tools.versions;- imagem Docker sem tag ou com tag sem
x.y.z(postgres:16), noDockerfile, nocompose.yaml, nos acessórios dodeploy.ymle nos workflows. Com digest, passa. Uma imagem que só publicax.yentra emeng/pinned-versions.exceptions, exata e com a razão; npx <pacote>sem@x.y.zem scripts, workflows,Makefile, scripts dopackage.jsone nos blocos de código do Markdown. Sem versão passa só o pacote que umpackage.jsondo repositório já declara com versão exata;runs-on: ubuntu-latestouuses: action@vNsemvX.Y.Znem o SHA completo;- a mesma imagem com versões diferentes entre
compose.yaml,deploy/*/config/deploy.yml, osDockerfile, os workflows e o código (ex.: Keycloak do Testcontainers × Keycloak do Compose).
Lacunas e variáveis (${X}) ficam de fora: as lacunas são do gaps.sh, e a variável vem da
fonte única. Um ecossistema que falte entra no script por mudança na arquitetura, com teste.
O roteiro da Fase 1 (docs/guias/definir-a-stack.md)
O modelo traz o roteiro que o mantenedor e o agente seguem, em conversa:
Critérios mínimos que toda escolha de tecnologia precisa responder (vão para o "Contexto" da ADR de stack):
| Critério | Pergunta |
|---|---|
| OIDC | Pergunta: Há cliente OIDC maduro com Authorization Code + PKCE e leitura de client roles? |
| Observabilidade | Pergunta: Dá para cumprir o contrato (JSON no stdout, Prometheus, OTLP) sem agent externo? |
| Testes | Pergunta: Há como subir banco e Keycloak reais em container nos testes de integração? |
| Container | Pergunta: Gera uma imagem enxuta, sem estado, que roda como usuário não-root? |
| Design system | Pergunta: Consegue servir a cópia de vendor/hibex/ e renderizar o HTML de referência? |
| Time | Pergunta: Quem vai manter conhece? Há material atualizado para a versão escolhida? |
| Ciclo de vida | Pergunta: A versão escolhida tem suporte (LTS) pelo horizonte do produto? |
O que o modelo não decide e não deixa como lacuna
Algumas decisões são da stack, mas surgem ao longo da vida, não no nascimento. O modelo não as força na Fase 1 — elas viram ADRs próprias quando aparecerem:
- formato de API entre aplicações (se um dia houver);
- fila, cache, busca, agendador;
- armazenamento de arquivos (o padrão Hibex até aqui é só links, sem upload);
- internacionalização;
- integrações externas específicas.