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

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 e a maioria governada por uma ADR: ① run() nunca lança, ② agnóstico a adapter/store, ③ IDs content-addressed + run-dir determinístico, ④ o dissenso é preservado pelo Verifier. São as regras que todo pacote mantém — a razão de o sistema ser testável, reproduzível e confiável. Conhecê-las é conhecer a espinha do motor.

Leia primeiro (fonte primária)
docs/alembic-complete-map.md §2 — "The 4 invariants"

Esta lição destila o §2 do mapa como-construído, com cada invariante verificada contra os arquivos citados (packages/*/src + as ADRs). Por que importa: são o filtro que diz se um pacote pertence ao Alembic. Se ele viola uma das quatro, não é Alembic — é só código que mora no mesmo repositório.

Uma nota sobre "seis". Uma versão mais antiga deste curso dizia "seis invariantes". Isso estava desatualizado. O mapa como-construído (docs/alembic-complete-map.md §2) lista exatamente quatro — a contagem abaixo é a autoritativa. Confundir "princípios" extras com as invariantes que sustentam carga é precisamente o tipo de divergência que o método de engenharia reversa (lição 20) existe para pegar.
Objetivos desta lição
  • Nomear as quatro invariantes e apontar o arquivo+linha que afirma cada uma.
  • Explicar por que run() nunca lançar e o ID ser content-addressed se reforçam mutuamente (replay).
  • Distinguir "agnóstico a adapter/store" de "só uma interface" — o teste é rodar em memória com fakes.
  • Mostrar por que o dissenso é estrutural (Verifier read-only + contrarian-last no board-load), não um prompt.
0
invariantes (não 6, não 3)
0
governadas por ADR (0009, 0003)
0+
testes que rodam em memória, $0
0
hex do hash SHA-256 do spec

As quatro, em resumo

Comece pelo papel que elas cumprem. As quatro invariantes são o filtro de pertencimento: dado um pacote qualquer do repositório, elas decidem se ele é Alembic ou apenas código que mora no mesmo lugar. Pense num portão de quatro perguntas — basta reprovar em uma e o pacote não passa:

PORTÃO DE PERTENCIMENTO · "este pacote é Alembic?" — reprovar em UMA invariante já barra
um pacote qualquer ① run() nunca lança? ② agnóstico a adapter/store? ③ IDs content- addressed? SIM SIM SIM ④ dissenso estrutural? SIM ✓ É ALEMBIC passou nas quatro ✗ NÃO É ALEMBIC é só código que mora no mesmo repositório qualquer "NÃO" leva aqui — basta reprovar em UMA NÃO NÃO NÃO As quatro são conjuntivas (E lógico): não há "3 de 4". É o que o método de eng. reversa (lição 20) verifica no código.

Passado o portão, vale ver como as quatro se organizam. Elas não são uma lista solta: são dois pares que se reforçam. ① e ③ (nunca-lança + content-addressed) tornam a run reproduzível; ② e ④ (agnóstico + dissenso estrutural) tornam a run testável e confiável. Pense numa grade 2×2 — reprodutibilidade no eixo de cima, confiança no de baixo:

AS 4 INVARIANTES EM GRADE 2×2 · cada quadrante afirmado no código, a maioria com ADR
↑ tornam a run REPRODUZÍVEL ↑ run() nunca lança resultado = união discriminada uniforme um erro vira um VALOR tipado, não uma exceção — a fronteira nunca quebra runWithGuards · ADR-0009 IDs content-addressed run-dir determinístico ⇒ replay o ID É o hash do spec; mesmo input, mesmo ID, mesmo lugar no disco runIdFor(spec) agnóstico a adapter/store kernel puro, efeitos colaterais injetados nenhum motor IMPORTA um concreto; tudo chega como argumento → fakes AdapterRegistry · FsPort injetados dissenso pelo Verifier estrutural, não por um prompt Verifier read-only + oráculos determinísticos; contrarian-last no load verifier.ts · board.ts · ADR-0003 ↓ tornam a run TESTÁVEL & CONFIÁVEL ↓

E por que "afirmada no código" importa tanto? Porque há uma escala de força. Uma invariante pode viver só num comentário (frágil), num teste (melhor), ou no tipo / no carregamento — onde o compilador ou o motor a recusam antes de rodar. As quatro do Alembic vivem no degrau mais alto: são impostas por construção, não por boa-fé.

ESCALA DE FORÇA DE UMA REGRA · da mais frágil à mais forte (as 4 vivem no topo)
+ frágil → + forte (impossível de ignorar) comentário convenção teste verde imposto no tipo / no load ← as 4 ① tipo discriminado + runWithGuards · ② assinaturas que exigem injeção · ③ hash de conteúdo · ④ erro duro de board-load. Nenhuma depende de alguém "lembrar de seguir a regra". Por isso o método de eng. reversa (lição 20) procura a regra no código, não no README.
Preveja antes de continuar
Quantas das quatro invariantes você acha que são impostas por um prompt dado ao modelo (em vez de pelo código que roda)? Chute antes de revelar.
Zero. Nenhuma das quatro depende de um prompt. ① é imposta por runWithGuards (código), ② por injeção de dependência (assinaturas de função), ③ por um hash de conteúdo (runIdFor), e ④ — a mais contraintuitiva — por um erro duro de carregamento (contrarian_not_last) e por um Verifier que é read-only por arquitetura. Se você chutou "uma" ou "duas", a lição inteira é sobre por que a resposta é zero: prompts podem ser ignorados; o código, não.

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

A ideia em uma frase. Imagine um caixa eletrônico que nunca trava na sua frente: deu erro de rede, saldo insuficiente, cartão inválido — ele sempre devolve um cartãozinho com a resposta, jamais uma tela azul. Em vez de uma exceção que pode derrubar quem chamou, toda chamada de modelo no Alembic devolve um valor que diz "deu certo" ou "deu errado, eis o porquê". Quem chamou só precisa olhar o cartãozinho — nunca precisa de um try/catch de emergência.

Você viu isto na lição 14. É imposto estruturalmente por runWithGuards (adapter-core.ts:118), que envolve validação de input, circuit breaking e retry/backoff e devolve sempre um ModelRunResult discriminado em ok — nunca lança. Verificado em produção: o handoff registra um 429 real surgindo como falha tipada (HANDOFF.md:274). 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 ("The narrow waist — a model call never throws and returns a discriminated result").

O contraste é o coração do invariante. À esquerda, o mundo "normal": uma exceção sobe pela pilha e quem não a tratou quebra. À direita, o mundo Alembic: o erro é capturado na fonte e desce como um valor que o chamador tem de olhar:

EXCEÇÃO QUE SOBE vs VALOR QUE DESCE · o mesmo 429, dois mundos
SEM o invariante (throw) gateway responde HTTP 429 throw sobe pela pilha… ✗ chamador sem try/catch QUEBRA a run inteira pode cair COM o invariante (runWithGuards) gateway responde HTTP 429 runWithGuards captura na fonte ✓ { ok:false, error } — um VALOR o chamador ramifica no resultado Mesmo 429 real (HANDOFF.md:274). À direita ele nunca vira exceção — vira um campo que o tipo te obriga a tratar.
// a forma do resultado: discriminada em `ok` — nunca um throw
type ModelRunResult =
  | { ok: true;  value: Completion }
  | { ok: false; error: ModelError };   // um 429 chega aqui, não como exceção

E não é só o adapter. O invariante se repete em camadas: o adapter garante a fronteira na chamada de modelo; o núcleo (core.ts) re-estabelece a mesma garantia em volta de council e swarm com runDebateSafe/runSwarmSafe; o servidor (lição 17) a fecha de novo em cada handler HTTP. Um throw em qualquer profundidade é capturado pela fronteira mais próxima e desce como valor:

A FRONTEIRA REPETIDA EM CAMADAS · um throw nunca escapa do nível em que nasce
servidor HTTP — cada handler → status tipado (nunca lança) núcleo — runDebateSafe / runSwarmSafe (core.ts:313-334) adapter — runWithGuards aqui o throw é capturado throw (429) ↩ vira { ok:false } — não sobe

② 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" (etl/src/index.ts:11-14). 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).

DEPENDÊNCIA IMPORTADA (proibido) vs DEPENDÊNCIA INJETADA (a lei) · o mesmo motor, dois grafos
✗ import de concreto DebateEngine import direto cliproxyapi (rede real) amarrado ao concreto: impossível testar sem socket ✓ injeção (Alembic) DebateEngine(adapters) argumento fake (em teste) registry offline $0, hermético o mesmo motor aceita real, fake ou offline — só troca o argumento
// 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.

O ganho é concreto. Compare um motor amarrado a concretos (precisa de gateway no ar, rede, dinheiro, e fica lento e instável) com o motor injetável do Alembic (fake em memória: rápido, determinístico, $0):

TESTE AMARRADO À REDE vs TESTE EM MEMÓRIA · o que a injeção compra
amarrado a concreto (rede) injetado (fake em memória) velocidade lento (I/O) rápido determinismo flaky (rede) estável custo $ por chamada $0 pré-requisito gateway no ar + token nenhum

③ 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 (swarm/src/types.ts:247-257: "Changing any field yields a new run directory").

DO SPEC AO LUGAR NO DISCO · spec → hash → run-dir (mude um campo e tudo muda)
spec (imutável) goal · tasks · maxDepth · maxConcurrency runIdFor SHA-256 do conteúdo <baseDir>/runs/<runId>/ events.jsonl (append-only) · checkpoint.json mude UM campo do spec… …novo hash → NOVO diretório (nada sobrescreve o antigo) input idêntico → mesmo hash → MESMO diretório → replay

A consequência que importa é o replay. Reexecutar o mesmo spec não cria uma run nova: o hash bate, o run-dir é o mesmo, e o motor retoma/re-deriva ali — convergência. Mude um campo e o hash diverge: nasce um diretório novo, e o antigo fica intacto. Os dois caminhos, lado a lado:

REEXECUTAR O MESMO SPEC vs MUDAR UM CAMPO · convergência (replay) vs novo diretório (fork)
mesmo spec, de novo spec idêntico → hash a1b2… runs/a1b2…/ (o MESMO diretório) ✓ REPLAY — converge retoma do checkpoint / re-deriva igual um campo mudou spec alterado → hash 9f7e… runs/9f7e…/ (diretório NOVO) runs/a1b2…/ continua intacto ↪ FORK — nada sobrescrito o id É o conteúdo, então mudou o id
// 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

Repare na peça que fecha o ciclo: os stores também são content-addressed. canonicalJson ordena as chaves recursivamente e contentHash tira o SHA-256 disso; se o hash já existe no arquivo, appendStoreRecord não escreve de novo — é um no-op (etl/src/stores.ts:79-97,148-166). Conteúdo igual nunca duplica:

APPEND CONTENT-ADDRESSED · escrever o MESMO registro duas vezes = uma linha só
appendStoreRecord(payload) hash = SHA-256(canonicalJson) hash já existe no arquivo? SIM no-op written:false NÃO grava linha (atômico) writeFileAtomic · written:true

É 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).

Laboratório: o hash decide o run-dir

Sinta o invariante ③ na mão. Ligue/desligue campos do spec abaixo: cada mudança recalcula um hash de demonstração e, com ele, o caminho do run-dir. Repare em duas coisas — o mesmo conjunto sempre dá o mesmo hash (reprodutível), e qualquer alteração dá um hash totalmente diferente (novo diretório, nada sobrescrito).

SPEC → HASH → RUN-DIR · alterne os campos e veja o caminho mudar
spec:
hash de conteúdo (demo · 12 hex): run-dir resultante:

Selecione um conjunto de campos acima.

Hash de demonstração ≠ SHA-256 real
O laboratório usa um hash curto só para ilustrar a relação spec → caminho. No código, runIdFor usa um SHA-256 de 64 caracteres hex sobre o JSON canônico do spec. A lição é a mesma: a identidade da run É o seu conteúdo — não um contador, não um timestamp, não um UUID aleatório.

④ O 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 por dois mecanismos:

O segundo mecanismo merece um diagrama, porque é onde "contrarian-last" deixa de ser uma frase e vira execução. As fases do debate rodam em série (uma depois da outra), mas os membros dentro de uma fase rodam em paralelo. O contrarian é alocado na última fase — então, por construção, ele só fala depois que todos os outros já se posicionaram. Não há prompt pedindo isso: é a ordem em que o motor executa.

FASES SERIAIS, MEMBROS PARALELOS · o contrarian fala por último porque roda por último
→ fases em SÉRIE (uma só começa quando a anterior termina) → fase 1 membro A membro B A e B em paralelo ↕ fase 2 membro C membro D C e D em paralelo ↕ última fase contrarian vê tudo que veio antes sozinho, por último ✓ Posição garantida pela ordem de execução — e contrarian_not_last é erro de board-load, então a config inválida nem carrega.
PROMPT "SEJA CONTRARIAN" vs DISSENSO ESTRUTURAL · por que um pode ser ignorado e o outro não
✗ dissenso por prompt prompt: "discorde, seja o crítico" o modelo obedece? pode racionalizar e concordar um pedido, não uma garantia ✓ dissenso estrutural Verifier read-only · oráculos determinísticos sobre a evidência contrarian-last imposto no LOAD contrarian_not_last = erro duro ✓ a pressão não pode ser ignorada é propriedade do sistema (ADR-0003)

Por que "oráculo determinístico" e não "o Verifier lê o argumento do maker"? Porque um oráculo é uma função pura sobre a evidência estruturada — mesma evidência entra, mesmo veredito sai, sem rede e sem modelo. Ele olha votos, scores, spread e quórum; nunca a prosa. Veja a anatomia de uma prova de claim:

ANATOMIA DE UM ORÁCULO · evidência estruturada entra → veredito determinístico sai (prosa nunca entra)
evidência estruturada votos · scores spread · quórum (VerifierEvidence) oráculo (função pura) sem I/O · sem modelo proven: true | false + a evidência consultada ✗ prosa do maker — BARRADA
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 "the bug was fixed" — proveniência acima de polimento. Governado pela ADR-0003 ("Dissent is a system property, not a council Role"): não há Role "contrarian" privilegiado; a pressão adversarial é trabalho do Verifier.

As quatro juntas: rastreie uma run (passo a passo → agora você)

As invariantes não agem isoladas — elas se encadeiam numa run real. Siga uma chamada do começo ao fim e veja cada uma entrar em cena. Depois, um caso é seu. Recuperar o encadeamento (não só ler a lista) é o que fixa de verdade.

Exemplo resolvido · uma run de council bate num 429 — quais invariantes seguram?
1
O spec vira identidade (③). A run nasce de um spec; runIdFor(spec) tira o SHA-256 e fixa o run-dir <baseDir>/runs/<runId>. Mesmo spec ⇒ mesmo lugar ⇒ a run é reproduzível desde o byte zero.
2
O motor recebe tudo injetado (②). runDebate({ board, pack, adapters, requestId }) — o AdapterRegistry chega como argumento. Em teste seria um fake; em produção, o real. O motor não sabe nem se importa qual.
3
O gateway responde 429 (①). Dentro do adapter, runWithGuards captura e devolve { ok:false, error } — um valor, não uma exceção. O runDebateSafe do núcleo (core.ts:313-334) garante a fronteira mesmo se algo lançasse mais fundo.
4
O Verifier julga sem confiar em prosa (④). Quando o debate produz uma decisão, o Verifier read-only prova os claims com oráculos determinísticos sobre votos/scores — e o contrarian já falou por último, porque o board-load recusaria o contrário.
5
Resultado. Nada quebrou, nada não-determinístico entrou, e o run-dir guarda o registro append-only. Reexecutar o mesmo spec converge para o mesmo lugar — replay, não um fork novo.
Agora você: um alembic.plan.ts contém uma linha const seed = Math.random(). Qual invariante isso viola, e o que o motor faz? Pense antes de revelar.
Viola o invariante : Math.random() torna o spec não-determinístico, então o hash (e o run-dir) mudaria a cada execução, quebrando replay e cache. A VM recusa carregar o plano — ela rejeita Date.now(), new Date() e Math.random() de propósito (lição 17/28). Dica: sempre que algo muda o conteúdo do spec sem o usuário mudar a intenção, suspeite do invariante ③.

Quatro, não seis (e nem as três do funil)

Duas armadilhas de contagem rondam estas invariantes. A primeira: o curso antigo dizia "seis" — divergência pura. A segunda é mais sutil e vale fixar: o funil tem suas próprias três invariantes de segurança (docs/alembic-complete-map.md §3) — orçamento que degrada em vez de estourar, escritas append-only/content-addressed/validadas, e leituras read-only na fonte. Essas três são locais ao funil; as quatro desta lição são arquiteturais e valem para todo pacote. Não as some.

O erro de soma fica óbvio quando você desenha os escopos lado a lado: conjuntos diferentes, abrangências diferentes — e um deles simplesmente não existe.

TRÊS CONTAGENS, TRÊS ESCOPOS · 4 arquiteturais (§2) vs 3 do funil (§3) vs "seis" (fantasma)
4 · arquiteturais §2 — vale para TODO pacote ① run() nunca lança ② agnóstico a adapter/store ③ IDs content-addressed ④ dissenso estrutural 3 · segurança do funil §3 — SÓ o funil (L0→L3 ETL) orçamento degrada, não estoura escritas append-only/CAS/validadas leituras read-only na fonte "seis" não existe — resquício de um curso desatualizado ≠ 4 + algo extra Conjuntos disjuntos: 4 (todo pacote) e 3 (só o funil) NÃO se somam em 7 — e "seis" nunca foi um conjunto real.
ConjuntoQuantasEscopoOnde está
Invariantes arquiteturais4Todo pacote do Alembicalembic-complete-map.md §2
Invariantes de segurança do funil3Só o funil (L0→L3 ETL)alembic-complete-map.md §3
"Seis invariantes"não existe (curso antigo, desatualizado)
Como não se confundir: pergunte "isto vale para todo pacote ou só para um?". As quatro são propriedades que qualquer motor do Alembic mantém. As três do funil descrevem como aquele pipeline específico se mantém seguro sob orçamento. Contagens diferentes, escopos diferentes — citar a §2 ou a §3 evita o erro.

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.
"run() nunca lança é só try/catch em todo lugar." É o oposto: o try/catch fica num único lugar (runWithGuards) e some do resto do código. O chamador ramifica num valor (ok: true | false), não embrulha cada chamada — por isso o tipo te obriga a tratar a falha.
"Dissenso é o prompt do contrarian." Não — um prompt pode ser ignorado. O dissenso é o Verifier read-only com oráculos determinísticos mais o contrarian-last checado no load. A ADR-0003 faz dele uma propriedade do sistema, não um papel que se pede ao modelo.

Fixe os conceitos (flashcards)

Clique pra virar. Tente lembrar a resposta — e o arquivo que a afirma — antes de virar. Recuperação ativa fixa mais que reler.

Invariante ①
O que garante que run() nunca lança?
clique pra virar ↻
Resposta
runWithGuards (adapter-core.ts:118): devolve um ModelRunResult discriminado em ok. Um 429 vira { ok:false }. ADR-0009.
Invariante ②
Qual é o "teste" de agnóstico a adapter/store?
clique pra virar ↻
Resposta
Rodar o motor inteiro em memória com fakes. AdapterRegistry + FsPort chegam injetados; nenhum import de concreto.
Invariante ③
Por que o id da run é content-addressed?
clique pra virar ↻
Resposta
runIdFor(spec) = SHA-256 do spec. Input igual → mesmo id → mesmo run-dir → replay. Por isso o plano precisa ser determinístico.
Invariante ④
Por que o dissenso é estrutural, não um prompt?
clique pra virar ↻
Resposta
Verifier read-only + oráculos determinísticos (verifier.ts:19-99) e contrarian_not_last como erro duro de board-load (board.ts:142-149). ADR-0003.

Como isso se encaixa

As quatro invariantes não são uma seção isolada do curso: são o chão sobre o qual toda run do Alembic anda. Onde você está na metodologia? Bem embaixo dela. Cada estágio que você já viu — o spec virar run-dir, o motor chamar um adapter, o council debater, os gates julgarem, o loop de aprendizado registrar — repousa sobre uma destas quatro regras. Tire uma e o estágio correspondente perde a garantia: a run pode quebrar (①), virar não-testável (②), divergir a cada execução (③) ou aceitar um consenso sem pressão (④).

O diagrama abaixo mostra o encaixe literal. Em cima, o fluxo de uma run da esquerda para a direita. Embaixo, as quatro invariantes como uma fundação — cada uma sobe e segura o estágio que governa. Use os botões para acender invariante por invariante e ver o que cada uma sustenta.

AS 4 INVARIANTES COMO FUNDAÇÃO · o fluxo da run anda em cima; cada regra segura um estágio
→ o FLUXO de uma run (a metodologia que você percorre no curso) → spec → run-dir runIdFor · lição 28 motor + adapters stores injetados · lição 14 council debate · lição 18 gates proof/validator · lição 17 registro + aprendizado append-only · lições 26/8 ↑ cada invariante SEGURA o estágio acima dela ↑ ▼ a FUNDAÇÃO: as 4 invariantes (o que esta lição nomeia) ▼ ① nunca lança runWithGuards · ADR-0009 segura: motor + council + gates a fronteira nunca quebra ② store-agnóstico AdapterRegistry · FsPort segura: motor + stores testável em memória, $0 ③ content-addressed runIdFor · determinismo segura: run-dir + registro replay converge ④ dissenso Verifier read-only · ADR-0003 segura: council a pressão é estrutural Sem a fundação, o fluxo de cima não tem garantia. É por isso que o método de eng. reversa (lição 20) procura estas quatro no código antes de chamar um pacote de "Alembic".
acenda:

Clique numa invariante para ver qual estágio do fluxo ela segura — ou "passo a passo" para percorrer as quatro.

Onde você está na metodologia
Estas quatro invariantes ficam abaixo de tudo o que o mapa interativo da metodologia mostra: elas são pré-condições, não etapas. Quando você seguir o pipeline em o mapa da metodologia (interativo), lembre-se de que cada caixa daquele mapa só funciona porque uma destas quatro a sustenta. Conhecer a fundação é conhecer por que o resto se mantém de pé.

As invariantes conectam-se a quatro outras lições — cada uma aprofunda uma face desta fundação:

PARA ONDE ESTA LIÇÃO APONTA · siga o fio de cada invariante

Na prática

Invariante não é teoria de quadro-branco — você consegue tocar duas delas com a CLI, sem custo. O invariante ③ (content-addressed/determinístico) aparece no alembic replay: reexecutar o mesmo run-id retoma o mesmo diretório em vez de criar um novo. E o invariante ① (nunca lança) é exatamente o que a suíte de testes prova — toda falha vira valor tipado, então o teste verifica ramos, não captura exceções.

Comece pelos dois mais fáceis de provar. ③ pela identidade de run-dir (replay), ① pela suíte verde. ② e ④ vivem nos testes de pacote (@alembic/council, @alembic/etl): rodam em memória justamente porque ② vale, e checam o contrarian-last porque ④ é estrutural.

① nunca lança — a suíte verde é a prova

O baseline de build/teste do projeto roda mais de 400 testes em memória, $0, sem rede. Eles só conseguem rodar assim porque os erros são valores (①) e as dependências são injetadas (②):

# baseline do repositório — tudo verde a cada mudança (CLAUDE.md)
pnpm -r typecheck && pnpm -r build && pnpm -w test

# saída esperada (resumida):
#  ✓ packages/adapters  runWithGuards: um 429 vira { ok:false } (não lança)
#  ✓ packages/council   contrarian_not_last = erro duro de board-load
#  ✓ packages/etl       appendStoreRecord: conteúdo idêntico = no-op
#  Test Files  N passed
#  Tests       400+ passed

Para isolar só um invariante, rode o pacote que o afirma — a saída exata varia com a versão; o que importa é "passed":

# o invariante ① / ② no adapter (a fronteira que não lança)
pnpm --filter @alembic/adapters test
# o invariante ④ no council (Verifier + contrarian-last)
pnpm --filter @alembic/council test
# o invariante ③ no etl (append content-addressed / no-op)
pnpm --filter @alembic/etl test

③ content-addressed — veja o replay convergir

Aqui o invariante ③ fica visível a olho nu. Uma run nasce de um spec; seu id é o hash do conteúdo. Reexecutar pelo mesmo id não bifurca — retoma o mesmo run-dir a partir do events.jsonl + cache:

# 1) liste as runs existentes (cada id É o hash content-addressed do spec)
alembic runs list

# 2) reexecute UMA pelo seu id — replay, não uma run nova
alembic replay <run-id>

# saída esperada (o ponto é a IDENTIDADE do diretório):
#  resume/re-execute from events.jsonl + cache
#  → mesmo <run-id> ⇒ MESMO ~/.alembic/runs/<run-id>/ (nada sobrescrito)
#  conteúdo idêntico re-anexado = no-op (convergência, não duplicação)
Por que isto prova ③, e não só "tem cache"
O sinal do invariante é que o caminho do diretório não muda entre a run original e o replay — porque o id é derivado do conteúdo do spec, não de um relógio nem de um contador. Se você editasse um campo do spec e rodasse de novo, veria um novo id e um novo diretório (um fork), com o antigo intacto. Identidade estável = invariante ③ em ação.
Experimente · provar ① e ③ com a sua cópia do repositório
1
Entre no repositório e instale. cd para a raiz do monorepo Alembic e rode o baseline: pnpm -r typecheck && pnpm -r build && pnpm -w test. Observe a contagem de testes terminar em passed — isso é o invariante ① (nenhuma exceção escapa) e ② (tudo em memória) provados de uma vez.
2
Isole o invariante ④. Rode pnpm --filter @alembic/council test e procure o caso de contrarian_not_last: o board inválido nem carrega. É o dissenso como erro de load, não como prompt.
3
Veja o content-addressing. Rode alembic runs list para ver os ids (cada um é um hash). Pegue um e rode alembic replay <run-id>. O que procurar: o diretório retomado é ~/.alembic/runs/<run-id>/ — o mesmo id, o mesmo caminho. Reexecutar não criou um diretório novo. Esse é o invariante ③ a olho nu.
Sem nenhuma run ainda? Crie uma offline e hermética primeiro — por exemplo alembic distill <corpus> --offline (lição 15) — e depois volte ao passo 3. [uncertain] o nome exato do diretório-base pode variar conforme --data-dir/configuração; o invariante a observar é a estabilidade do caminho entre run e replay, não a string literal.

Revisão cumulativa — recupere de memória

Antes de clicar: responda de cabeça. As opções têm tamanhos parecidos de propósito — sem pista pela forma.

1. Quantas invariantes arquiteturais o mapa como-construído nomeia?
Correto: c. O §2 do mapa lista quatro, cada uma com citações. a é a contagem antiga e desatualizada ("seis") — exatamente a divergência que a lição corrige. b confunde invariantes com ADRs: só duas das quatro são governadas por ADR (0009 e 0003); as outras são afirmadas direto no código. d nega o fato central — a contagem é fixa e autoritativa: quatro.
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. a erra o motivo: o problema não é desempenho, é identidade instável. c é falso — ambas existem no Node; a VM as bloqueia de propósito. d troca o tema: PII é proveniência/segurança (lição 26), não determinismo. Determinismo é pré-condição do invariante ③.
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. a erra: tamanho de modelo não cria pressão adversarial — a separação read-only sim. b está errado porque um prompt é um pedido e o board-load é uma garantia checada. c inventa uma mecânica (rodar duas vezes) que não existe; o que existe é o Verifier independente sem superfície de mutação. ADR-0003 faz disso uma propriedade do sistema.
4. Você lê que o "funil tem três invariantes de segurança". Como isso convive com as quatro desta lição?
Correto: a. As quatro (§2) valem para todo pacote; as três do funil (§3 — orçamento que degrada, escritas append-only/content-addressed/validadas, leituras read-only) descrevem só aquele pipeline. b soma escopos diferentes — o erro clássico de contagem que a seção "quatro, não seis" alerta. c inverte a hierarquia: o local não substitui o arquitetural. d é falso — não há contradição; são granularidades distintas, cada uma citada em sua seção.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Por que o Verifier não pode chamar um modelo?", "O que é JSON canônico, na prática?", "Como o 429 do handoff prova o invariante ①?". É só dizer.