Por que os sete subsistemas da fusão se parecem tanto? Porque cada um obedece a uma disciplina: depender de portas injetadas, retornar Result em toda fronteira, validar entrada não confiável com Zod, e nunca lançar exceção. A disciplina não é decoração — é o que torna o motor testável, determinístico e agnóstico de store (ADR-0009).
Tudo nesta lição é citado de arquivo real do monorepo Alembic — nenhum trecho é inventado. O eixo: o tipo Result<T, Error> de @alembic/contracts e as quatro portas que recorrem por todo o pacote @alembic/hermes (FsPort, Clock, idFactory, e os backends de modelo/rede). A regra de ouro vem do CLAUDE.md: "fail-closed Result<T, Error>… evitar lançar em código de biblioteca".
Result<T, Error> e por que ele torna a falha um valor que o compilador obriga você a tratar — e não uma exceção que escapa.FsPort, Clock, idFactory e um backend/adapter.Clock e idFactory são a mesma ideia: injetar a coisa que tornaria uma run irrepetível (a VM de plano proíbe Date.now()/Math.random()).safeParse vira err em vez de um throw.Quando cinco subsistemas diferentes têm o mesmo formato, não é coincidência — é uma disciplina imposta de propósito. Aprenda a disciplina uma vez e você lê qualquer pedaço da fusão sem reaprender nada.
Pense num restaurante com cinco cozinheiros. Se cada um inventa sua própria forma de pegar ingredientes, medir o tempo e provar o prato, a cozinha vira um caos imprevisível. Mas se todos seguem o mesmo protocolo — "peça o ingrediente à despensa (não vá buscar sozinho), olhe o relógio da parede (não chute a hora), e devolva 'pronto' ou 'queimou' (nunca jogue a panela no chão)" — então qualquer cozinheiro entende a estação de qualquer outro na hora.
Pense como… a diferença entre cinco pessoas improvisando e cinco pessoas seguindo a mesma checklist. A checklist da fusão tem quatro itens: (1) não construa suas próprias dependências — receba-as; (2) devolva Result, nunca lance; (3) substitua o que é não-determinístico (tempo, ids) por uma costura injetada; (4) valide o que vem de fora na fronteira. A analogia quebra num ponto: na cozinha o protocolo é cortesia; aqui é imposto pelo sistema de tipos — o código nem compila se você ignorar o Result.
A disciplina tem dois pilares ortogonais. O pilar de erro: todo código de biblioteca retorna Result<T, Error> e nunca lança (regra do CLAUDE.md). O pilar de portas: nenhum subsistema constrói seu próprio fs, relógio, gerador de id ou cliente de rede — ele declara uma interface (a porta) e o chamador injeta a implementação. Os dois se cruzam: cada porta também retorna Result, então a falha de uma dependência injetada é, ela também, um valor.
O ADR-0009 ("cintura estreita / nunca-lança / agnóstico de store") é o documento que amarra isso. Ele explica por que o mesmo kernel pode rodar contra uma API de fronteira, um modelo local MLX ou um fake offline sem mudar uma linha — e por que o Validador real do coda pode substituir o portão padrão por injeção.
throw em código de bibliotecaTudo se apoia num tipo minúsculo de @alembic/contracts. Um Result é um valor que é ou sucesso ou falha — nunca uma exceção.
Imagine duas caixas etiquetadas. A caixa Ok guarda o valor que deu certo. A caixa Err guarda o erro que deu errado. Uma função devolve uma das duas — e na frente de cada caixa há uma etiqueta booleana, ok, dizendo qual é. Você não pode abrir a caixa e pegar o valor sem antes olhar a etiqueta. É isso que muda tudo: a falha deixa de ser uma surpresa que explode no meio do caminho e vira um dos dois resultados possíveis, ambos previstos.
ok diz qual caixa você tem antes de poder abri-la.if (!r.ok) return r; é tudo: depois dele, ler r.value é seguro e checado pelo compilador.Aqui está o tipo inteiro — minúsculo de propósito:
packages/contracts/src/result.ts:10-26export interface Ok<T> { readonly ok: true; readonly value: T; } export interface Err<E> { readonly ok: false; readonly error: E; } export type Result<T, E = Error> = Ok<T> | Err<E>; export const ok = <T>(value: T): Ok<T> => ({ ok: true, value }); export const err = <E>(error: E): Err<E> => ({ ok: false, error });
O booleano ok é uma tag de união discriminada: após if (!r.ok) return r; o compilador sabe que o resto é r.value. Não há terceiro estado nem fluxo de controle oculto — uma falha é um valor que você deve tratar, não uma exceção que desenrola a pilha.
Se código de biblioteca nunca lança, onde fica o único try/catch de todo o sistema?
tryCatchAsync. Esse é o único lugar onde uma chamada que pode lançar (uma SDK de terceiro, um JSON.parse) é encapsulada e o throw é convertido em err. A partir dali, para dentro, tudo é Result. A função até se documenta sozinha: o comentário diz "Never rejects."O único lugar onde um try/catch cabe é bem na borda, encapsulando uma chamada que lança de volta no contrato:
/** Encapsula uma função async que lança como Result. Nunca rejeita. */ export const tryCatchAsync = async <T>( fn: () => Promise<T>, onError: (cause: unknown) => Error = toError, ): Promise<Result<T, Error>> => { try { return ok(await fn()); } catch (cause) { return err(onError(cause)); } };
Result está bem ali. É a frase-âncora desta lição inteira. Tudo o que vem a seguir é uma consequência dela.
À esquerda, a exceção escapa da pilha — fácil de esquecer. À direita, a falha está no tipo: o compilador não te deixa ler .value antes de tratar !r.ok.
CLAUDE.md): "fail-closed Result<T, Error> … evitar lançar em código de biblioteca". Se o código de biblioteca não pode lançar, um chamador nunca pode ser surpreendido por uma exceção — todo caminho de falha está no tipo. Os sete subsistemas da fusão honram isso sem exceção.Uma porta é uma interface injetada — o subsistema declara do que precisa e o chamador fornece. Nenhum subsistema constrói seu próprio filesystem, relógio, modelo ou cliente de rede.
"Injetar uma dependência" parece intimidante, mas é uma ideia do dia a dia. Quando você liga um abajur, você não embute uma usina de energia dentro dele — você pluga numa tomada. A tomada é a porta: o abajur só declara "preciso de 127V aqui"; quem fornece a energia é a casa. Troque a casa (ou ligue num gerador, ou numa bateria de teste) e o abajur funciona igual, sem mudar nada nele. Um subsistema da fusão é o abajur; FsPort, Clock, idFactory e o backend são as tomadas.
Quatro costuras recorrem por toda a fusão:
| Porta | O que abstrai | Prod vs teste |
|---|---|---|
FsPort | IO de filesystem (read/write/escrita-atômica) | impl real node:fs · fake em memória |
Clock | o tempo atual | relógio do sistema · relógio fixo/avançável |
| backend / adapter | uma chamada de modelo ou provedor de rede | fetch/ModelAdapter · um fake enlatado |
idFactory | cunhar identificadores | contador monotônico (determinístico em todo lugar) |
O kernel no centro não importa fs, nem clock, nem nenhum SDK — só as portas. Em oliva, as costuras de determinismo; em slate, as de rede/modelo.
options) primeiro. As dependências que ele recebe são o mapa exato do que aquele subsistema toca no mundo externo. Se um node:fs ou Date.now() aparecesse dentro do kernel, seria um cheiro de código — a disciplina foi quebrada.
A teoria fica concreta quando você vê o mesmo formato repetido em cinco arquivos reais. Use as abas do explorador para acender cada porta no diagrama, depois leia o trecho de código que a prova.
O construtor recebe seu filesystem, o caminho do arquivo e seu relógio. Não constrói nenhum deles.
curator/usage-store.ts:58-63 prod: node:fs + relógio do sistema · teste: fake em memória + relógio fixoO construtor recebe seu filesystem, o caminho do arquivo e seu relógio. Não constrói nenhum deles:
packages/hermes/src/curator/usage-store.ts:58-63export class UsageStore { constructor( private readonly fs: FsPort, // IO injetado — sem node:fs private readonly sidecarPath: string, // o caminho é argumento, sem home global private readonly clock: Clock, // tempo injetado — sem Date.now() ) {}
Escritas atômicas passam por FsPort.writeFileAtomic para que um crash nunca deixe um sidecar meio-escrito; leituras são best-effort (um arquivo corrompido é tratado como vazio, retornando ok, então uma chamada de telemetria de hot-path não pode quebrar o host).
| Operação | O que acontece se falhar | Por quê |
|---|---|---|
| Leitura de um sidecar corrompido | ok(vazio) | telemetria de hot-path não pode derrubar o host |
| Escrita atômica que falha | err | perder uma escrita é um erro real que o chamador deve ver |
A assimetria é deliberada: leitura corrompida → ok(vazio), escrita falha → err. Decidir qual falha é fatal e qual é tolerável é metade da arte de uma porta bem desenhada.
ok vazio), perder uma escrita é fatal (err). A porta codifica essa decisão.O kernel web não importa nenhum SDK e nenhum backend concreto. Ele declara duas costuras injetadas — o provedor e um compressor opcional — ambas retornando Result:
export interface WebBackend { search(query: WebSearchQuery): Promise<Result<readonly WebSearchResult[], Error>>; extract(url: string): Promise<Result<WebExtractResult, Error>>; } // Costura opcional de compressão LLM — encapsula UMA chamada de ModelAdapter // em prod; ausente ⇒ conteúdo bruto é retornado sem alteração. export type Compressor = ( text: string, instruction: string, ) => Promise<Result<string, Error>>;
Uma falha de rede é err; uma busca vazia é ok([]) — a diferença é preservada no tipo. Esta é exatamente a costura que o ciclo de aprendizado usa no seu ReviewProposer: "uma chamada de modelo em prod, um fake em testes".
A primitiva bloqueante de humano-no-loop precisa de ids. Ela recebe um id factory injetado, com padrão de contador monotônico — nunca Math.random() ou Date.now(), que a VM de plano do motor rejeita e que quebraria o replay:
constructor(options: ClarifyGatewayOptions = {}) { this.mintId = options.idFactory ?? monotonicIdFactory(); // injetado, padrão determinístico } export const monotonicIdFactory = (prefix = 'clarify'): (() => ClarifyId) => { let n = 0; return () => { n += 1; return `${prefix}-${n}`; }; };
Node não tem thread bloqueante, então o gateway é uma promise + registro de resolvers + timeout: ask() registra uma entrada pendente sob um id cunhado, arma um setTimeout, e retorna a promise aguardada. No timeout a entrada é descartada e a promise resolve para err — nunca trava e nunca lança.
ok se respondem, em err no timeout.Clock e idFactory são o mesmo padrão. Ambos substituem um global não-determinístico proibido — Date.now() e Math.random() — por uma costura injetada, pelas mesmas duas razões: testabilidade e replay (a VM de plano rejeita ambos os globais). A fonte torna o elo explícito: o doc-comment da própria porta Clock (curator/types.ts:147-154) referencia monotonicIdFactory. Então a metade de determinismo desta disciplina é uma ideia com duas instâncias — injete a coisa que de outro modo tornaria uma run irrepetível.ask(question). O gateway cunha um id pelo factory injetado: monotonicIdFactory() devolve clarify-1 (determinístico — não Math.random()).clarify-1 num mapa de resolvers e arma um setTimeout. Retorna a promise aguardada — o kernel fica esperando, sem travar a thread.setTimeout dispara: a entrada clarify-1 é removida e a promise resolve para err(...). Nada lança, nada fica pendurado.idFactory que sempre devolve clarify-1 e um relógio fixo. Por que isso torna o teste do timeout 100% reproduzível? (Resposta: id e tempo deixam de ser fontes de variação — a mesma sequência de eventos produz exatamente o mesmo err, toda vez, sem sleep real.)Qualquer coisa de fora do programa — a proposta de um modelo, a resposta de clarify de uma plataforma, o JSON de um backend — é não confiável. Então é validada com Zod na fronteira, e uma falha vira err, não uma exceção lançada.
Pense num porteiro na entrada de um prédio. Tudo que vem da rua passa por ele: ele confere o documento antes de deixar entrar. Se o documento está rasgado ou falso, o porteiro não chama a polícia gritando (não "lança uma exceção") — ele simplesmente devolve a pessoa com um "barrado". Zod é esse porteiro: safeParse confere a forma do dado na porta de entrada; se não bate, vira err e o passo falha fechado, sem deixar lixo entrar no sistema.
safeParse é o porteiro: válido entra como ok, inválido vira err. Nunca um throw, nunca um cast cego.const parsed = clarifyQuestionSchema.safeParse(question); if (!parsed.success) { return err(new Error(`Invalid clarify question: ${parsed.error.message}`)); } // …e em learning/review.ts:83-85, a saída do proposer (modelo não confiável) é // reviewProposalSchema.safeParse'd antes de qualquer escrita. Mesma forma, toda fronteira.
safeParse'd antes de qualquer escrita — nunca um as que finge confiança que não existe.
Toda essa disciplina compra três coisas concretas. Não são abstrações de arquiteto — cada uma resolve um problema real de quem mantém o sistema.
FsPort fake + um Clock fixo + um backend enlatado e o kernel roda com zero IO, zero rede, zero instabilidade — e zero não-determinismo de Date.now().Date.now()/Math.random()), então as runs são reproduzíveis e o replay bate.Duas objeções aparecem sempre que alguém vê esse padrão pela primeira vez. Ambas têm uma resposta curta e firme.
Result é só exceção com passos extras." Não: a exceção é como um alçapão escondido no chão — você não vê na planta da casa e cai nele sem aviso. O Result é uma porta marcada "saída de emergência": está no mapa, e você é obrigado a decidir se vai passar por ela.Result é só exceção com passos extras." A diferença é o sistema de tipos. Uma exceção é invisível na assinatura de uma função; um Result<T,Error> está bem ali, e o compilador não te deixa ler .value até você ter tratado !r.ok. A falha se torna impossível de esquecer.Passe pelo deck para fixar a sequência, vire os cartões para praticar recuperação, e leve as dez ideias.
Vire cada cartão (clique, ou Enter/Espaço) e tente responder antes de ver o verso. É prática de recuperação — vale mais que reler.
Result<T, Error> e qual é a regra de ouro?Ok<T> | Err<E> com a tag ok. Regra: código de biblioteca retorna Result e nunca lança.try/catch do sistema?tryCatchAsync, na borda — ele converte um throw em err. Comentário: "Never rejects."FsPort (IO), Clock (tempo), idFactory (ids) e um backend/adapter (modelo/rede). Todas injetadas.Clock e idFactory são a mesma ideia?Date.now()/Math.random()) por uma costura injetada — para o replay bater e o teste ser reproduzível.UsageStore, leitura corrompida e escrita falha retornam o quê?ok(vazio) (telemetria não derruba o host). Escrita falha → err (erro real). Assimetria deliberada.safeParse'd por Zod na fronteira (reviewProposalSchema). Forma inválida → err, falha fechada. Nunca um as cego.Result<T, Error> é Ok | Err: a falha é um valor, não uma exceção que escapa.ok estreita o tipo: depois de if (!r.ok) return r;, ler r.value é seguro.try/catch é tryCatchAsync na borda.FsPort, Clock, idFactory e um backend/adapter.Clock e idFactory são a mesma ideia: injetar o não-determinístico para o replay bater.ok(vazio); escrita falha → err.safeParse na fronteira → inválido vira err.Três perguntas. Escolha e leia o porquê de cada opção — o feedback ensina tanto quanto a pergunta.
Checagem cumulativa
Acerte as três para fechar a lição. A pontuação aparece abaixo.
Result e nunca lança. O único try/catch está dentro de tryCatchAsync bem na borda, que converte um throw em err. O chamador trata a falha como um valor que o tipo o força a considerar. (a) viola a regra; (c) esconde o erro num ok falso.UsageStore recebe um Clock no construtor em vez de chamar Date.now()?reviewProposalSchema.safeParse) antes de qualquer escrita. Uma proposta malformada vira err — nunca uma exceção lançada (a), nunca confiança presumida (b), nunca um cast cego (d).