Por que todos os sete subsistemas da fusão se parecem? 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 se apoia num tipo minúsculo de @alembic/contracts. Um Result é um valor que é ou sucesso ou falha — nunca uma exceção:
// packages/contracts/src/result.ts:10-26 export 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.
O único lugar onde um try/catch cabe é bem na borda, encapsulando uma chamada que lança de volta no contrato:
// packages/contracts/src/result.ts:57-69 — "Never rejects." /** 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<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 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. Quatro costuras recorrem na 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 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-63 export 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). A assimetria é deliberada: leitura corrompida → ok(vazio), escrita falha → err.
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:
// packages/hermes/src/web/types.ts:120-140 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:
// packages/hermes/src/clarify/gateway.ts:72-74 + 176-182 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.
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.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:
// packages/hermes/src/clarify/gateway.ts:86-89 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.
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.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.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, nunca um cast não verificado.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.