Pular para o conteúdo

Padrões de codificação e qualidade

Status: corrigir · Escopo: Padrões de código, camadas e organização do monorepo · Atualizado em: 2026-06-21

Normas obrigatórias para o voltron. Objetivo: monorepo coeso, domínios com fronteiras rígidas, testes confiáveis e CI que bloqueia regressão. Alinhado ao blueprint.md (§4.4, §10, §20) e ao roadmap (E0.T1–T2).


PrincípioRegra
Modular monolithUm deploy Worker + um serviço LLM; separação lógica por bounded context, não por microserviço prematuro
TS orquestra, Python geraWorkers decidem estratégia; Python faz NLU/geração — sem lógica de negócio duplicada entre runtimes
Domínio no centroRegras de negócio vivem em domain/; infra (storage, filas, HTTP) fica nas bordas
Contratos explícitosToda fronteira (domínio↔domínio, Worker↔LLM, Worker↔DB) tem tipo/schema versionado
Fail closedConsent, privacy, safety e idempotência negam por padrão — nunca “seguir em frente” em dúvida
Observabilidade by defaultToda decisão relevante emite timeline_event; toda chamada LLM gera llm_call
Zero import acidentalFronteiras de import são verificadas automaticamente (CI), não por review humano

/
├── apps/
│ ├── worker/ # Cloudflare Workers (TypeScript)
│ │ └── src/
│ │ ├── channel/
│ │ ├── identity/
│ │ ├── conversation/
│ │ ├── orchestration/
│ │ ├── policy/
│ │ ├── workflow/
│ │ ├── memory/
│ │ ├── knowledge/
│ │ ├── privacy/
│ │ ├── safety/
│ │ ├── actions/
│ │ ├── quality/
│ │ ├── storage/ # repositórios Postgres/R2 — única camada com SQL
│ │ ├── observability/
│ │ ├── jobs/ # consumers de fila
│ │ └── api/ # rotas HTTP (webhooks, health, admin)
│ └── llm/ # Python — Pydantic AI
│ └── src/
│ ├── prompting/
│ ├── grounding/
│ ├── media/
│ ├── guardrails/
│ ├── observability/
│ ├── evals/
│ └── schemas/ # contratos Pydantic (fonte de verdade semântica)
├── packages/
│ ├── contracts/ # JSON Schema / OpenAPI gerados → tipos TS
│ └── test-fixtures/ # factories, seeds, golden payloads
├── schema/
│ └── migrations/ # DDL versionado (data-model-v1)
├── docs/
│ ├── engineering/ # este doc + ADRs
│ └── architecture/
├── pyproject.toml # uv workspace root
├── pnpm-workspace.yaml
├── turbo.json
└── biome.json / eslint.config.*

Regras de pasta

  • Cada bounded context em apps/api-worker/src/<context>/ segue a mesma forma interna (§3).
  • Nada de lógica de domínio em api/ ou jobs/ — só adaptadores (parse, auth, enqueue, dispatch).
  • SQL somente em apps/api-worker/src/storage/ (TS) e, se necessário no futuro, apps/llm-service/src/observability/ para writes isolados de llm_calls.
  • Arquivos compartilhados entre contextos TS → packages/contracts ou tipos em storage expostos via interface — nunca import cruzado de domain/ de outro contexto.

<context>/
├── domain/ # entidades, value objects, regras puras (sem I/O)
├── application/ # use cases / services — orquestra domain + ports
├── ports/ # interfaces (repositórios, gateways externos)
└── index.ts # public API do módulo — único ponto de export externo
CamadaPode importarNão pode importar
domain/outros domain/ do mesmo contexto, tipos de packages/contractsstorage, api, jobs, outros contextos, SDKs (Meta, Neon, R2)
application/domain/, ports/ do mesmo contexto, packages/contractsimplementações concretas de storage, outros contextos (exceto via port injetada)
ports/tipos de domínio, contratosimplementações
index.tsre-exporta só o que outros contextos precisam

Regra de ouro: outro contexto só importa de @worker/<context> (barrel index.ts). Importar de @worker/<context>/domain/... ou @worker/<context>/application/...proibido (lint falha).

3.1 Grafo de dependências entre contextos (Worker)

Seção intitulada “3.1 Grafo de dependências entre contextos (Worker)”

Espelha blueprint.md §10. Setas = “pode depender de”.

flowchart TD
channel --> identity --> conversation
conversation --> orchestration
memory --> orchestration
knowledge --> orchestration
policy --> orchestration
workflow --> orchestration
privacy --> policy
orchestration --> safety
safety --> actions
orchestration --> observability
observability --> quality
storage -.->|implementa ports| channel
storage -.->|implementa ports| identity
storage -.->|implementa ports| conversation
storage -.->|implementa ports| memory
storage -.->|implementa ports| orchestration
storage -.->|implementa ports| observability
ContextoDepende deObservação
channelidentity, storage, observabilityGateway + idempotência inbound
identitystorage, observabilityResolução tenant/unit/contact
conversationstorage, observabilityEstado, bursts, mensagens
orchestrationconversation, memory, knowledge, policy, workflow, storage, observabilityNúcleo — não chama Meta/LLM direto
policyprivacy, storageRegras configuráveis
workflowstorageStage engine
memorystorage, observability4 camadas + commit gate
knowledgestorageRAG index/retrieve
privacystorageConsent, erasure
safetycontrato LLM (HTTP), observabilityValida saída antes de actions
actionschannel (outbound port), storageOutbox + entrega
qualitystorage, observabilityEval/replay
observabilitystorageWriters de timeline/logs/llm_calls
storageFolha — nenhum contexto de domínio
api, jobsqualquer index.ts de contextoAdaptadores finos

Proibido

  • storage importar qualquer contexto de domínio
  • domain/ importar @cloudflare/workers-types bindings diretamente — injetar via ports/
  • orchestration importar SDK da Meta ou cliente HTTP do LLM — usar ports (LlmGateway, ChannelOutbound)
  • Ciclo entre contextos (A→B→A)

Stack HTTP: FastAPI + Uvicorn. Handlers finos; lógica em prompting/, guardrails/, etc.

apps/llm-service/src/tactflow_llm/
├── api/ # FastAPI app, routes, deps (auth) — sem regra de negócio
├── schemas/ # Pydantic — fonte de verdade dos contratos Worker↔LLM
├── prompting/ # agents Pydantic AI
├── grounding/ # embeddings + retrieval
├── media/ # transcrição / visão
├── guardrails/ # validação pós-geração
├── observability/ # writer llm_calls
└── evals/ # Pydantic Evals
CamadaRegra
schemas/Sem dependência de FastAPI/HTTP; só Pydantic + stdlib
api/Rotas FastAPI finas — validam schema, chamam service, retornam schema
prompting/, guardrails/, etc.Importam schemas/; não importam api/

Proibido em Python

  • Reimplementar regras de orquestração (timing, burst, window 24h, consent) — isso é TS
  • Acesso direto a tabelas que não sejam llm_calls / chunks de knowledge (se explicitado)
  • Import de código TypeScript

  1. Fonte semântica: apps/llm-service/src/tactflow_llm/schemas/ (Pydantic v2).
  2. Derivação TS: gerar tipos em packages/contracts via script (pnpm contracts:gen) — JSON Schema ou OpenAPI.
  3. Versionamento: campo contract_version em payloads críticos; breaking change = nova versão + compat temporária.
  4. Transporte: HTTP via FastAPI (Worker → LLM_SERVICE_URL) — body validado Pydantic nos dois lados; auth service-to-service via X-API-Key.

Model routing (OpenRouter): uma API key, modelo por slot de pipeline (UNDERSTAND, GENERATE, GUARD, …) — defaults no código, override por OPENROUTER_MODEL_<SLOT>, override por tenant via policy_profiles (Phase 1). Ver apps/llm-service/README.md. Não usar um único OPENROUTER_MODEL global. 5. Erros: resposta tipada { error_code, message, retryable } — nunca string solta.

Endpoints mínimos v1 (nomes ilustrativos):

EndpointRequestResponse
POST /v1/understandburst normalizado + context packUnderstandingResult
POST /v1/generateplano + context pack + groundingGenerationResult
POST /v1/guardtexto candidato + contextoGuardResult

RegraDetalhe
Single SQL layer (TS)Queries só em storage/<context>/ — um repositório por agregado
Tenant scope obrigatórioToda query inclui tenant_id; unit-scoped inclui unit_id quando aplicável
Sem ORM pesadoSQL explícito ou query builder tipado fino (ex.: Drizzle/Kysely) — decisão no E0, mas nunca SQL inline fora de storage/
TransaçõesUse case em application/ abre transação via UnitOfWork port; repos não iniciam transação sozinhos
IdempotênciaWrites críticos passam por idempotency_keys — teste de integração obrigatório
ParticionamentoRepos encapsulam created_at para routing de partição — callers não escolhem partição
R2Keys namespaced: {tenant_id}/{unit_id}/{conversation_id}/... — sem PII em path quando evitável

Proibido

  • orchestration/application/decide.ts com sql`SELECT ...`
  • Retornar row bruto do DB para camada de API — mapear para tipos de domínio/contrato

FerramentaUso
pnpmPackage manager + workspaces
TurboOrquestração de build/test/lint
TypeScript 5.xstrict: true, noUncheckedIndexedAccess, exactOptionalPropertyTypes
BiomeLint + format (preferido) ou ESLint flat + Prettier — um par, não dois
VitestUnit + integration (Workers: @cloudflare/vitest-pool-workers quando aplicável)
dependency-cruiserValida grafo de imports entre contextos
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"verbatimModuleSyntax": true
}
}
  • Named exports — evitar default export (exceto configs)
  • Sem anyunknown + narrow; @typescript-eslint/no-explicit-any: error
  • Sem barrel files gigantesindex.ts exporta só API pública (~≤15 exports)
  • Erros tipadosResult<T, E> ou exceptions de domínio mapeadas em boundary; não engolir erro
  • Datas — sempre Date em domínio, timestamptz ISO string nos contratos wire
  • IDs — tipo branded TenantId, ConversationId (UUID v7 string) — não string cru
  • Handlers HTTP ≤ ~30 linhas — delegam para use case
  • Sync boundary curta no webhook (ack + enqueue) — regra do blueprint §4.7
  • DO (ConversationRuntimeDO) — estado efêmero + coordenação; persistência via use cases, não lógica de negócio inline no DO
  • Bindings injetados — não acessar env globalmente dentro de domain/

FerramentaUso
uvProjeto, lock, scripts, CI
RuffLint + format (ruff check, ruff format)
tyType check (ty check)
pytestTestes
pytest-covCobertura
import-linterFronteiras entre pacotes Python
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "TCH", "RUF", "ASYNC", "S"] # S = bandit rules
ignore = []
[tool.ruff.lint.isort]
known-first-party = ["tactflow_llm"]
[tool.ruff.format]
quote-style = "double"
  • Python ≥3.12
  • Tipagem explícita em funções públicas; from __future__ import annotations
  • Pydantic models para I/O; dataclasses/NamedTuple para internals leves
  • Async onde houver I/O (HTTP LLM providers)
  • Logging estruturado (JSON) — campos: trace_id, tenant_id, conversation_id, model, latency_ms

NívelO quêOndeGate
UnitRegras puras (domain/), gates, parsersVitest / pytestCI sempre
ContractSchemas TS↔Pydantic, snapshots JSONpackages/contractsCI sempre
IntegrationRepos + Neon branch efêmeraschema/ harness + repo testsCI sempre
WorkerHandlers, DO, queue consumer (pool Workers)apps/api-workerCI sempre
E2EWebhook → fila → persist → outbound mockpoucos, críticosCI main + nightly
EvalPydantic Evals golden setsapps/llm-service/evalsCI main (amostra) + release
EscopoLinesBranchesNotas
Global repo80%70%Exclui configs, generated, migrations
domain/ + application/90%80%Onde mora a regra de negócio
privacy/, safety/, idempotência100%95%Fail-closed — sem exceção
storage/75%Integration compensa
Python guardrails/, schemas/90%80%

Exclusões permitidas no relatório (não na disciplina): arquivos *.generated.ts, schema/migrations/, **/index.ts re-export only.

  • Arquivo espelha produção: decide.tsdecide.test.ts / test_decide.py
  • Factories em packages/test-fixtures — não copiar JSON inline em 10 testes
  • Nomenclatura: describe('<Context>/<UseCase>') + it('should <behavior> when <condition>')
  • Testes de idempotência: mesmo input 2× → 1 side effect
  • Testes de tenant isolation: tenant A nunca lê/escreve tenant B
  • Sem teste que depende de ordem — parallel safe

Ver integration-testing.md.

  • Integração: repos + Neon (*.integration.test.ts, tests/integration/, @pytest.mark.integration)
  • Matriz de paths: schema/integration-paths.yml — todo fluxo ponta a ponta mapeado a testes
  • Colunas: pnpm schema:columns — 360 colunas classificadas (used | harness | system | deferred + justificativa)
  • Nova coluna DDL → atualizar migration + column-usage.yml + refs de runtime

Ordem sugerida (falha rápida):

# .github/workflows/ci.yml (alvo pós-scaffold)
jobs:
contracts: # pnpm contracts:gen && git diff --exit-code
ts-boundaries: # dependency-cruiser + eslint-plugin-boundaries
ts: # biome check + tsc + vitest --coverage
py-boundaries: # lint-imports
py: # ruff check + ruff format --check + ty check + pytest --cov
schema: # Neon branch → migrations → canonical_queries + invariants (E1.T4)
CheckComando (raiz)Bloqueia merge
Format/lint TSpnpm lint
Typecheck TSpnpm typecheck
Test + cov TSpnpm test -- --coverage
Import boundaries TSpnpm boundaries
Format/lint Pyuv run ruff check . && uv run ruff format --check .
Typecheck Pyuv run ty check
Test + cov Pyuv run pytest --cov --cov-fail-under=80
Import boundaries Pyuv run lint-imports
Schema harnesspnpm schema:validate (dev branch) / CI schema:validate:ci✅ quando secrets Neon configurados
Column auditpnpm schema:columns
Integration (TS+Py)pnpm test:integration✅ no job schema (Neon)
Contracts driftpnpm contracts:gen && git diff --exit-code packages/contracts

Branch protection (GitHub): status check Quality gate (blocking) required; no direct push to main. Em repo privado sem Pro, enforce via review + CI verde obrigatório antes de merge.


Arquivo .pre-commit-config.yaml na raiz:

HookFerramenta
Trailing whitespace / EOFpre-commit built-in
TS format+lintBiome (staged files)
Py format+lintRuff (staged files)
Secretsgitleaks ou detect-secrets
SQL migrationssqlfluff lint (opcional v1)

Instalação automática: pnpm install roda preparescripts/install-git-hooks.mjs (uv sync --group dev + pre-commit install). Manual: pnpm setup:hooks. Pule com SKIP_HOOKS=1.


Todo use case que decide algo deve:

  1. Propagar trace_id (header inbound ou gerado no webhook)
  2. Emitir timeline_event com event_type canônico (ver data-model-v1.md)
  3. Log estruturado em runtime_logs em falha ou latência anômala

Toda chamada LLM (Python):

  1. Registrar llm_calls antes e depois (status, tokens, latency, prompt_version)
  2. Nunca logar prompt completo com PII em produção — usar redaction helper

ÁreaRegra
PIIHelpers de redaction/masking centralizados em privacy/
SecretsSó via env/bindings — nunca commit; CI scan
ErasureUse case único privacy/eraseContact — único lugar que anonimiza + dispara purge R2
ConsentChecado em policy antes de outbound não-transacional
LogsSem telefone/nome/email em plaintext — hash ou truncar

ItemConvenção
Tabelas/colunas DBsnake_case (já no data model)
TS types/interfacesPascalCase
TS fileskebab-case.ts
Python modulessnake_case.py
Event typessnake_case verb phrases: burst_sealed, memory_committed
Env varsSCREAMING_SNAKE
Feature flagstenant.settings.<flag> — não env global por tenant

Commits: Conventional Commitsfeat(orchestration):, fix(storage):, test(privacy):.


SQL no orchestratorRepo em storage/orchestration/
Meta API call fora de channel/actionsPort ChannelOutbound
LLM call direto do DOUse case → HTTP LLM service
Import @worker/memory/domain/...Import @worker/memory
Regra de negócio no handler HTTPHandler → application service
environment ignorado em query de analyticsFiltro explícito prod vs simulation
Teste sem assert de tenant scopeSempre 2 tenants no teste de isolamento
any / # type: ignore sem ticketTipar ou ADR temporário com expiry

Decisões que afetam fronteiras ou toolchain → docs/live/engineering/adrs/NNNN-título.md (template: contexto, decisão, consequências).

Mudança neste documento → PR dedicado + bump de versão no rodapé.


Referência cruzada com roadmap:

  • E0.T1 — scaffold monorepo conforme §2
  • E0.T2 — pyproject.toml (uv), Ruff, ty, pytest; pnpm + Biome; Vitest
  • dependency-cruiser config espelhando §3.1
  • import-linter config espelhando §3.2
  • Scripts raiz: lint, typecheck, test, boundaries, contracts:gen, quality
  • CI substituindo job placeholder (ci.yml)
  • .pre-commit-config.yaml
  • Coverage thresholds enforced (§8.2) — escopo atual (storage + LLM stubs)
  • README apontando para este doc
  • E0.T4 completo — repos Postgres por contexto + writer Python llm_calls

Versão 1.0 — 2026-05-30