Lição 16 · Curso de Fusão · Parte 3 · A arquitetura · As quatro invariantes
Parte 3 · A arquitetura · Lição 16

As quatro invariantes

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

Cada bloco tem uma versão Simples e uma Técnica. Abra a técnica quando quiser o código real.
O que presumimos de você
  • Que você já viu a cintura estreita (lição 14) e o funil (lição 15) — vamos nos apoiar nelas.
  • Que "invariante" pode soar sofisticado. Não é: é só uma regra que nunca pode ser quebrada. Explicamos cada uma do zero.
  • Nada além disso. Não é preciso saber TypeScript a fundo para entender por que essas quatro regras seguram o motor.
1

A grande ideia


Ao terminar esta lição você consegue
  • Nomear as quatro invariantes e dizer qual ADR governa cada uma.
  • Explicar por que cada uma torna o motor testável, replayável ou confiável.
  • Reconhecer a diferença entre dissenso de prompt (frágil) e dissenso estrutural (garantido).
  • Ligar a invariante ③ à regra "planos não podem usar Date.now()".

A maioria dos sistemas se descreve por uma lista longa de "princípios". O Alembic faz o contrário: ele se reduz a quatro propriedades que nunca são violadas. Tudo o mais — convenções, padrões de pasta, estilo de código — é consequência dessas quatro, não um quinto princípio.

Pense num prédio: ele não tem "cem boas ideias de engenharia". Ele tem fundação, estrutura, vedação e instalações — e o resto decorre disso. As quatro invariantes são os pilares; a casa inteira (os 19+ pacotes) fica de pé porque eles ficam.
Que invariante compra cada propriedade testável ② agnóstico replayável ③ por conteúdo confiável ① nunca-lança + ④ dissenso
As três propriedades do topo não são vagas — cada uma é comprada por uma invariante concreta.
Uma nota sobre "seis". Uma versão antiga deste curso dizia "seis invariantes". Estava desatualizada. O mapa as-built lista exatamente quatro — a contagem abaixo é a autoritativa. Confundir "princípios" extras com as invariantes que de fato sustentam o peso é precisamente o tipo de deriva que o método de reverse-engineering (lição 20) existe para pegar.
Infográfico de quatro pilares verticais sustentando uma arquitrave rotulada 'o motor: testável, replayável, confiável'. Pilar ① run() nunca lança (ADR-0009); pilar ② agnóstico a adapter e store; pilar ③ IDs endereçados por conteúdo; pilar ④ dissenso preservado pelo Verifier (ADR-0003). Faixa de fundação: 'cada uma afirmada no código-fonte, a maioria governada por um ADR'.

As quatro invariantes como quatro pilares — cada uma afirmada no código-fonte, a maioria governada por um ADR.

2

As quatro, num relance


Os quatro pilares do motor o motor — testável · replayável · confiável run() nunca lança união discriminada uniforme · ADR-0009 agnóstico a adapter & store kernel puro, efeitos injetados IDs por conteúdo run-dir determinístico ⇒ replay dissenso preservado pelo Verifier, não por um prompt · ADR-0003 fundação: cada invariante afirmada no código-fonte, a maioria governada por um ADR
Quatro pilares — leia da esquerda (①) para a direita (④). O resto da arquitetura repousa sobre eles.
Preveja antes de continuar

Das quatro, qual você acha que a maioria das equipes acha que tem, mas não tem de verdade?

A ④ — dissenso. Quase todo "council" adiciona um prompt "seja o advogado do diabo" e se diz adversarial. O Alembic não confia num prompt para isso: ele torna o dissenso estrutural. Voltamos a essa diferença na seção ④.
#InvariantePor que importaOnde se prova
run() nunca lançatodo resultado é um valor tipado — erros não escapam como exceçõesadapter-core.ts:118 · ADR-0009
agnóstico a adapter & storetodo engine roda em memória, com fakes, sem rededebate.ts:71-83 · funnel.ts:79-81
IDs por conteúdo + run-dir determinísticoentradas iguais convergem no mesmo id e lugar — runs são replayáveisorchestrator.ts:168
dissenso preservado pelo Verifiera pressão adversarial é propriedade do sistema, não um pedido ignorávelverifier.ts:19-99 · ADR-0003
3

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


Você já viu isto na lição 14. Quando você manda o motor rodar algo, ele nunca "estoura" uma exceção na sua cara. Ele sempre devolve um valor que diz, de forma uniforme, "deu certo, eis o resultado" ou "deu errado, eis o erro tipado". Um erro nunca é uma surpresa que escapa — é um dado que você inspeciona.

É a diferença entre um cano que estoura molhando a casa e um painel com luzinhas: a falha aparece num mostrador previsível, no mesmo formato de sempre, em vez de inundar tudo.

como é imposto

É imposto estruturalmente por runWithGuards (packages/adapters/src/adapter-core.ts:118) e verificado ao vivo — o handoff registra um 429 real aparecendo como uma falha tipada. O núcleo de orquestração restabelece a mesma fronteira uma camada acima com runDebateSafe / runSwarmSafe (packages/harness/src/core.ts:314-334), de modo que um throw dentro de um passo de council ou swarm também vira um valor. Governado por ADR-0009.

a forma da união discriminada
type Result<T, E> =
  | { ok: true;  value: T }     // caminho feliz
  | { ok: false; error: E };   // caminho de erro — tipado, nunca lançado

// quem chama SEMPRE checa o discriminante .ok antes de usar .value
const r = await runWithGuards(/* … */);
if (!r.ok) return r;        // a falha é um valor que você propaga
use(r.value);
Um throw entra; um valor tipado sai adapter lança ex.: HTTP 429 runWithGuards captura · classifica embrulha em Result ok:true · value ok:false · error uma camada acima: runDebateSafe / runSwarmSafe fazem o mesmo um throw num passo de council/swarm também vira valor · ADR-0009
A fronteira é restabelecida em cada camada — nada escapa como exceção não tratada.
Guarde isto Falha é dado, não evento. Você nunca embrulha uma chamada do motor em try/catch esperando uma surpresa — você checa .ok.
Como quem chama lida com a falha o jeito que NÃO usamos try { run() } catch (e) { … } depende de uma surpresa que pode escapar o jeito do Alembic const r = run(); if (!r.ok) return r; a falha é um valor que você propaga
O discriminante .ok torna impossível "esquecer" de tratar a falha — ela está no tipo.
4

② Engines são agnósticos a adapter E a store


Os kernels puros recebem apenas views readonly e efeitos colaterais injetados. Nenhum engine sai por conta própria para falar com um modelo de IA, com o disco ou com um banco. Tudo o que ele precisa chega como argumento. Troque o argumento por um fake e o engine inteiro roda em memória, de graça, sem rede.

É como uma cozinha profissional que não tem fornecedor fixo: os ingredientes chegam pela porta. Você pode entregar ingredientes de verdade (produção) ou de brinquedo (teste) — a receita é exatamente a mesma.

o padrão, em todo lugar: dependências chegam como argumentos, nunca como import de concretos

O DebateEngine, o scoring e o verifier recebem um AdapterRegistry injetado (packages/council/src/debate.ts:71-83). A camada ETL roteia todo o IO por um FsPort injetável — "every function here is testable in-memory" (packages/etl/src/index.ts:12). E o funil recebe um registry de adapters injetado, então trocar por um registry offline torna o run inteiro $0 e hermético (packages/harness/src/funnel.ts:79-81).

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

Isto é a lição 5 (ports & injeção) elevada a lei arquitetural: nenhum engine 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 que os subsistemas de @alembic/hermes (lições 7–13) fossem testados sem abrir um único socket.

Um kernel puro, duas fontes injetáveis engine puro só views readonly + efeitos injetados adapters reais produção · rede · disco fakes / offline teste · em memória · $0 mesmo comportamento, resultado verificável o engine não sabe — nem se importa — qual lado está plugado debate.ts:71-83 · etl/index.ts:12 · funnel.ts:79-81
"Agnóstico" não é só "tem uma interface" — é que o engine nunca importa o concreto.
Cartão · invariante ②
Qual é o teste prático de "agnóstico a adapter e store"?
clique para virar
resposta
Você consegue rodar qualquer engine inteiramente em memória, com fakes — sem rede e sem disco. Se precisa de um socket aberto para testar, a invariante foi quebrada.
5

③ IDs endereçados por conteúdo + layout determinístico de run-dir


O id de um run é o hash do conteúdo do seu spec (estilo SHA-256). Mude qualquer campo do spec e você ganha um diretório de run novo. Runs são replayáveis porque entradas idênticas convergem no mesmo id e no mesmo lugar no disco — rodar de novo retoma ou re-deriva o mesmo resultado em vez de criar um run paralelo.

É como a impressão digital de um documento: dois documentos idênticos têm a mesma digital e vão para a mesma gaveta. Trocou uma vírgula? Digital nova, gaveta nova. Nada se mistura por acaso.

content-addressed por construção

O id é resolvido por runIdFor(spec) (packages/swarm/src/orchestrator.ts:168) — um hash sobre o JSON canônico (chaves ordenadas) do spec. Os stores são endereçados por conteúdo por SHA-256 sobre esse mesmo JSON canônico, então re-anexar conteúdo idêntico é um no-op: re-runs convergem em vez de duplicar.

// run dirs: <baseDir>/runs/<runId> · events.jsonl append-only + checkpoint.json
// stores endereçados por SHA-256 sobre JSON canônico (chaves ordenadas) →
// re-anexar conteúdo idêntico é no-op → re-runs convergem, não duplicam
const runId = runIdFor(spec);   // muda 1 campo do spec ⇒ outro id ⇒ outro run-dir
Por isso planos têm que ser determinísticos

É por isto 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 se recusa a carregar um plano que os contenha (a lição 17 cobre o gate que impõe isso nos planos).

Entradas iguais convergem; uma mudança diverge spec X campos a,b,c spec X (de novo) campos a,b,c runIdFor → 7f3a… um id, um run-dir runs/7f3a… → retoma / re-deriva re-anexar idêntico = no-op (convergência) spec X' b alterado runIdFor → c01d… id novo, run-dir novo não-determinismo (Date.now/Math.random) ⇒ hash muda todo run ⇒ a VM rejeita o plano
O hash do conteúdo é o que liga "entrada idêntica" a "mesmo lugar no disco" — a base do replay.
Cuidado Um new Date() escondido num plano não dá erro "de lógica" — ele quebra o replay silenciosamente ao mudar o hash. Por isso a VM prefere recusar o plano a deixá-lo rodar.
6

④ O dissenso é preservado/forçado pelo Verifier, não por um mero prompt


Esta é a mais sutil — e a que mais equipes erram. Muitos "councils" adicionam um prompt dizendo "faça o advogado do diabo" e chamam isso de adversarial. O Alembic não confia num prompt para produzir dissenso — ele torna o dissenso estrutural, garantido pela arquitetura, não pela boa-vontade do modelo.

Pedir a alguém "por favor, discorde de você mesmo" é frágil — a pessoa pode ignorar. Ter um auditor independente que só pode checar fatos, sem poder mexer em nada, é garantido. O Alembic escolhe o auditor.

dissenso como propriedade do sistema

  • O Verifier maker-checker é read-only por arquitetura: ele aceita apenas views readonly, não expõe nenhuma superfície de adapter ou mutação, e prova claims atômicos com oráculos determinísticos sobre a evidência estruturada — nunca sobre a prosa do maker (packages/council/src/verifier.ts:19-99).
  • "Contrarian-last" é imposto no board-loadcontrarian_not_last é um erro DURO de carregamento (packages/council/src/board.ts:142-149) — e realizado como sequenciamento real pelo engine (fases seriais, membros paralelos).
O Verifier prova fatos, não acredita em prosa maker propõe + evidência estruturada Verifier (read-only) oráculos determinísticos sobre a EVIDÊNCIA ✗ sem mutação · ✗ sem adapter claim provado claim refutado contrarian_not_last = erro DURO no board-load (board.ts:142-149) a prosa do maker nunca é a fonte da verdade — a evidência estruturada é · verifier.ts:19-99
Read-only + oráculos determinísticos = um auditor que não pode ser convencido pela retórica.
Comparação em duas colunas: à esquerda, em vermelho-rust, 'o jeito frágil — um prompt' com um balão 'faça o advogado do diabo' e carimbo 'IGNORÁVEL'; à direita, em oliva, 'o jeito do Alembic — estrutura' com três cartões (Verifier read-only; oráculos determinísticos sobre evidência; contrarian_not_last é erro de board-load) e carimbo 'INEVITÁVEL'. Rodapé cita ADR-0003.

Dissenso de prompt (ignorável) vs. dissenso estrutural (garantido pela arquitetura). ADR-0003.

Um bug que foi corrigido, registrado com honestidade. O handoff já listou "contrarian-last é ficção (o prompt diz, o código roda em paralelo)" entre os bugs a não carregar adiante. No código atual isso não é mais ficção: é imposto no board-load e realizado por execução serial de fases. O mapa anota a divergência e diz "o bug foi corrigido" — proveniência acima de polimento. Governado por ADR-0003: não existe um Role "contrarian" privilegiado; a pressão adversarial é o trabalho do Verifier.
7

Explorador das invariantes


Clique em cada pilar para ver, lado a lado, o que ele garante, como é imposto e onde se prova. As quatro juntas são a espinha; remova uma e o motor deixa de ser testável, replayável ou confiável.

fundação comum — a espinha do motor
garante

① run() nunca lança

Todo resultado é um valor tipado: {ok:true,value} ou {ok:false,error}. Erros não escapam como exceções.

Imposto por: runWithGuards, mais runDebateSafe/runSwarmSafe uma camada acima.

adapter-core.ts:118 · harness/core.ts:314-334 · ADR-0009
8

Por que quatro — e não uma lista de boas práticas


Uma "boa prática" é um conselho: você deveria seguir. Uma invariante é uma lei: o sistema não te deixa quebrar. Essas quatro foram escolhidas porque cada uma compra uma propriedade que você consegue verificar — não admirar. Testável (②), replayável (③), confiável (① e ④). Se uma quinta "regra" não comprar uma dessas, ela é consequência, não pilar.

Cada invariante é afirmada no código, não só na prosa: ① no tipo de retorno e nos guards; ② na assinatura dos engines (dependências como parâmetros); ③ no runIdFor + na rejeição da VM; ④ no Verifier read-only + no erro de board-load. É isso que permite testá-las: uma invariante que não pode ser checada por um teste não é uma invariante, é uma intenção.

Pilar (lei) vs. consequência (decorre do pilar) os 4 pilares — ① nunca-lança · ② agnóstico · ③ por conteúdo · ④ dissenso plano determinístico decorre de ③ (não é pilar) suíte em memória, $0 decorre de ② (não é pilar) erro tratável como dado decorre de ① (não é pilar)
Se uma "regra" decorre de um pilar, ela é consequência — não um quinto pilar.
9

Exemplo guiado — classifique a violação


Um colega abre um PR com três mudanças. Para cada uma, qual invariante ela ameaça?

Diagnóstico passo a passo
1
Um engine novo faz import { realSlackAdapter } e o chama direto, sem recebê-lo por parâmetro. → Viola ②. O engine deixou de ser agnóstico; agora não dá para testá-lo em memória.
2
Um alembic.plan.ts calcula um nome de arquivo com Date.now(). → Viola ③. O hash do spec mudaria a cada run; a VM recusa o plano antes de rodar.
3
Uma função do council faz throw new Error('quota') em vez de devolver {ok:false,error}. → Viola ①. Quem chama esperava um valor; runDebateSafe existe justamente para reconverter isso, mas o código novo não deveria depender disso para "consertar" um throw evitável.
Da mudança à invariante ferida import realSlackAdapter; chamar direto fere ② agnóstico a adapter nome de arquivo via Date.now() fere ③ por conteúdo / determinismo throw new Error('quota') fere ① run() nunca lança
Treine o olho: cada cheiro de código aponta para exatamente uma invariante.
Agora você: imagine uma quarta mudança — "adicionei um prompt pedindo ao revisor para discordar mais". Qual invariante ela parece reforçar, mas na verdade não satisfaz? (Resposta: a ④. Um prompt é ignorável; só o Verifier read-only + o erro de board-load tornam o dissenso real.)
10

Confusões comuns


"Agnóstico a adapter é só ter uma interface." É mais: engines nunca importam um adapter, filesystem ou store concreto — eles chegam como argumentos injetados. O teste da invariante é conseguir rodar qualquer engine inteiramente em memória com fakes, o que a suíte faz o tempo todo.
"Endereçamento por conteúdo é para deduplicar." Dedupe é um benefício colateral. O propósito mais profundo é replay e convergência: entradas idênticas produzem o mesmo id e o mesmo lugar no disco, então um re-run retoma ou re-deriva o mesmo resultado em vez de bifurcar um novo.
"São quatro entre muitas boas práticas." Não. São as quatro. Outras regras (como o determinismo de plano) existem para servir uma destas — o determinismo é precondição da ③, não um quinto pilar.
"Tem interface" não basta — o teste é não importar o concreto só uma interface engine ainda faz import do adapter real → não dá para trocar por fake no teste ✗ invariante ② ferida dependência injetada adapter chega como argumento → teste passa um fake e roda em memória ✓ invariante ② cumprida
O critério é operacional: se você não consegue injetar um fake, a invariante não está lá.
AfirmaçãoFrágil?Estrutural?
"Prompt: seja o advogado do diabo"simnão
Verifier read-only com oráculos determinísticosnãosim
"Por favor, não use Date.now()"simnão
VM rejeita o plano não-determinísticonãosim
11

Recapitulando


Invariante ①

run() nunca lança

Todo resultado é uma união discriminada uniformeok:true/value ou ok:false/error. Falha é dado, não exceção. ADR-0009.

Result<T,E> ok:true · value ok:false · error
1
Invariante ②

Agnóstico a adapter & store

Kernels puros recebem views readonly + efeitos injetados. Por isso 400+ testes rodam em memória, sem socket.

engine puro reais (prod) fakes ($0) mesmo código
2
Invariante ③

IDs por conteúdo

O id do run é o hash do spec. Entradas iguais convergem; por isso planos têm que ser determinísticos.

spec X spec X (de novo) id 7f3a… um run-dir
3
Invariante ④

Dissenso estrutural

A pressão adversarial é garantida pela arquitetura (Verifier read-only + board-load), não por um prompt. ADR-0003.

prompt "discorde"ignorável Verifier read-onlyinevitável
4
A espinha

Quatro, não seis

Conhecer as quatro é conhecer o motor. Tudo o mais é consequência — não um pilar a mais.

o motor
5
Slide 1 / 5 use
12

Verifique


Revisão acumulada

Três perguntas. A pontuação corre conforme você responde — acerte as três e a revisão fecha.

1. Quantas invariantes arquiteturais o mapa as-built nomeia?
Correto: c. O §2 do mapa lista quatro. Uma versão antiga dizia "seis" — estava desatualizada. As quatro são as propriedades que sustentam o peso; todo o resto é consequência ou convenção.
2. Por que um módulo de plano precisa evitar Date.now() e Math.random()?
Correto: b. O id do run é um hash do spec; não-determinismo geraria um hash (e um run-dir) diferente toda vez, derrotando replay e cache. O determinismo é precondição da invariante ③, então o motor recusa planos não-determinísticos.
3. O que torna a invariante de "dissenso" do Alembic mais forte que um prompt "seja 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 do contrarian checada no load. O ADR-0003 faz da pressão adversarial uma propriedade do sistema, não um Role.
Acertos: 0/3
Você é o aluno e também o auditor: olhe um pacote qualquer do monorepo e pergunte — "esse engine importa algum concreto, ou recebe tudo por parâmetro?". Se importa, a invariante ② foi ferida. A seguir (lição 17): saímos das invariantes e entramos no pipeline de gates — Scope, Council, Proof, Validator e Publish — o maquinário que impõe essas regras em cada run.