Curso / Lição 16  ·  EN
Lição 16 · Motor & método · 3 de 8

Os quatro invariantes que mantêm o motor coeso

A arquitetura do Alembic repousa sobre exatamente quatro propriedades — não seis, não "boas práticas", mas quatro invariantes nomeados, cada um afirmado no código e a maioria governada por uma ADR. São as regras que todo pacote mantém, a razão de o sistema ser testável, reproduzível e confiável. Conhecê-los é conhecer a espinha do motor. Fonte: docs/alembic-complete-map.md §2, verificado contra os arquivos citados.

Uma nota sobre "seis". Uma versão mais antiga deste curso dizia "seis invariantes". Isso estava desatualizado. O mapa como-construído lista exatamente quatro — a contagem abaixo é a autoritativa. Confundir "princípios" extras com os invariantes que sustentam carga é precisamente o tipo de divergência que o método de engenharia reversa (lição 20) existe para pegar.

Os quatro, em resumo

① run nunca lança união discriminada uniforme · ADR-0009 ② agnóstico a adapter/store kernel puro, efeitos colaterais injetados ③ IDs content- addressed run-dir determinístico ⇒ replay ④ dissenso preservado pelo Verifier, não por prompt ADR-0003 quatro pilares — cada um afirmado no código, a maioria governada por uma ADR

① run() nunca lança; o resultado é uma união discriminada uniforme

Você viu isto na lição 14. É imposto estruturalmente por runWithGuards (adapter-core.ts:118) e verificado em produção — o handoff registra um 429 real surgindo como falha tipada. O núcleo de orquestração restabelece a mesma fronteira uma camada acima com runDebateSafe/runSwarmSafe (harness/src/core.ts:313-334), então um throw dentro de um passo de council ou swarm também vira um valor. Governado pela ADR-0009.

② Os motores são agnósticos a adapter E a store

Os kernels puros recebem apenas vistas readonly e efeitos colaterais injetados. O DebateEngine, o scoring e o verifier recebem um AdapterRegistry injetado (council/src/debate.ts:71-83). A camada ETL roteia todo IO por um FsPort injetável — "toda função aqui é testável em memória". E o funil recebe um registry de adapters injetado, então trocar por um registry offline torna a run inteira $0 e hermética (funnel.ts:79-81).

// o padrão, em todo lugar: dependências chegam como argumentos, nunca imports de concretos
runDebate({ board, pack, adapters, requestId });   // adapters injetados → teste com fakes
runT0Pipeline(corpusDir, { fs });                   // FsPort injetado → roda em memória
runFunnel(corpusDir, { adapters: offlineRegistry }); // registry offline → $0, hermético

Esta é a lição 5 (portas & injeção) elevada a lei arquitetural: nenhum motor alcança um adapter, filesystem ou store concreto. É exatamente o que faz a suíte de 400+ testes rodar rápido, em memória, sem rede — e o que permitiu testar os subsistemas do @alembic/hermes (lições 7–13) sem abrir um único socket.

③ IDs content-addressed + layout determinístico de run-dir

O id de uma run é o hash de conteúdo (estilo SHA-256) do seu spec: runIdFor(spec) (swarm/src/orchestrator.ts:168). Mude qualquer campo do spec e você obtém um novo diretório de run — runs são reproduzíveis porque inputs idênticos convergem para o mesmo id e o mesmo lugar no disco:

// run dirs: <baseDir>/runs/<runId> · events.jsonl append-only + checkpoint.json
// stores são content-addressed por SHA-256 sobre JSON canônico (chaves ordenadas) →
// re-anexar conteúdo idêntico é no-op, então re-runs convergem em vez de duplicar

É por isso que módulos de plano (alembic.plan.ts) precisam ser determinísticos — Date.now(), new Date() e Math.random() são rejeitados pela VM. Não-determinismo mudaria o hash do spec a cada run e quebraria o replay, então o motor recusa carregar um plano que o contenha (a lição 17 cobre o portão que impõe isso em planos).

④ Dissenso é preservado/forçado pelo Verifier, não meramente por um prompt

Este é o mais sutil, e o que mais times erram. Muitos "councils" adicionam um prompt que diz "faça advogado do diabo" e chamam isso de adversarial. O Alembic não confia num prompt para produzir dissenso — ele torna o dissenso estrutural:

Um bug que foi corrigido, registrado com honestidade. O handoff já listou "contrarian-last é ficção (o prompt diz, o código roda paralelo)" entre bugs a não levar adiante. No código atual já não é ficção: é imposto no board-load e realizado por execução serial de fases. O mapa nota a divergência e diz "o bug foi corrigido" — proveniência acima de polimento. Governado pela ADR-0003: não há Role "contrarian" privilegiado; a pressão adversarial é trabalho do Verifier.
1. Quantos invariantes arquiteturais o mapa como-construído nomeia?
Correto: c. O §2 do mapa lista quatro. Um curso antigo dizia "seis" — estava desatualizado. Os quatro são as propriedades que sustentam carga; tudo o mais é consequência ou convenção.
2. Por que um módulo de plano deve evitar Date.now() e Math.random()?
Correto: b. O id da run é um hash do spec; não-determinismo geraria um hash diferente (e run-dir diferente) toda vez, derrotando replay e cache. Determinismo é pré-condição do invariante ③, então o motor recusa planos não-determinísticos.
3. O que torna o invariante de "dissenso" do Alembic mais forte que um prompt "seja um contrarian"?
Correto: d. Um prompt pode ser ignorado ou racionalizado. O Alembic impõe o dissenso na arquitetura: um Verifier independente, sem mutação, com oráculos determinísticos, mais a ordenação contrarian checada no load. A ADR-0003 faz da pressão adversarial uma propriedade do sistema, não um papel.

Confusões comuns

"Agnóstico a adapter é só uma interface." Significa mais: motores nunca importam um adapter, filesystem ou store concreto — esses chegam como argumentos injetados. O teste do invariante é que você consegue rodar qualquer motor totalmente em memória com fakes, o que a suíte faz por toda parte.
"Content-addressing é para dedupe." Dedupe é um benefício colateral. O propósito mais profundo é replay e convergência: inputs idênticos produzem o mesmo id de run e o mesmo local no disco, então um re-run retoma ou re-deriva o mesmo resultado em vez de bifurcar um novo.