Lição 5 · Curso de Fusão · Parte 1 · O motor — Ports e injeção
Parte 1 · O motor · Curso de Fusão — Alembic × Hermes

Ports e injeção

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

Leia primeiro (fonte primária)
packages/contracts/src/result.ts + packages/hermes/src/{curator,web,clarify,learning}

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".

Leia a versão simples, ou abra a camada técnica em qualquer seção.
O que você vai conseguir fazer
  • Explicar o que é um 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.
  • Definir o que é uma porta injetada e nomear as quatro que recorrem na fusão: FsPort, Clock, idFactory e um backend/adapter.
  • Apontar por que 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()).
  • Justificar por que entrada não confiável é validada com Zod na fronteira, e por que safeParse vira err em vez de um throw.
  • Conectar os três payoffs — testabilidade, determinismo e agnosticismo de store (ADR-0009) — ao mesmo conjunto de decisões de design.
Suposições tolas (o que presumimos de você — bem pouco)
  • Você já viu uma função que pode falhar — uma leitura de arquivo, uma chamada de rede — e sabe que ela "às vezes dá errado".
  • Você consegue ler pseudocódigo simples. Não precisa saber TypeScript: vamos traduzir cada trecho linha a linha.
  • Você não precisa saber o que é injeção de dependência, união discriminada ou Zod. Construímos esses termos do zero aqui.
1

A disciplina sob tudo


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.

Por baixo do capô

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.

a checklist que os cinco subsistemas seguem — a mesma, sempre 1 · Receba dependências injetadas, não construídas 2 · Retorne Result nunca lance exceção 3 · Injete o não-determinístico Clock · idFactory (replay seguro) 4 · Valide na fronteira Zod safeParse untrusted → err
Quatro itens, um formato. Aprenda-os uma vez e você lê os sete subsistemas da fusão sem surpresa.
0
itens na disciplina (erro + 3 costuras)
0
subsistemas da fusão que a seguem
0
throw em código de biblioteca
2

O contrato: um Result que nunca lança


Tudo 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.

Result<T, E> = Ok<T> | Err<E> — uma união discriminada pela tag ok Ok<T> ok: true value: T Err<E> ok: false error: E | ou um, ou o outro
Não há terceiro estado. A etiqueta ok diz qual caixa você tem antes de poder abri-la.
como a tag estreita o tipo — type narrowing r : Ok<T> | Err<E> if(!r.ok) return r r : Ok<T> ⇒ r.value depois do early-return, o compilador garante: o resto do código só vê o caminho de sucesso.
O 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-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.

Faça uma aposta antes de continuar

Se código de biblioteca nunca lança, onde fica o único try/catch de todo o sistema?

Bem na borda — dentro de 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:

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)); }
};
o try/catch mora na borda — o interior é todo Result mundo que lança SDK · JSON.parse fetch que rejeita tryCatchAsync "Never rejects" interior do sistema só Result<T,E> nada lança aqui
Uma única fronteira encapsula tudo o que lança; para dentro dela, a falha já é um valor. É por isso que um chamador nunca é surpreendido.
Para gravar Uma exceção é invisível na assinatura; um Result está bem ali. É a frase-âncora desta lição inteira. Tudo o que vem a seguir é uma consequência dela.
Infográfico comparativo de dois painéis: à esquerda throw com a falha invisível escapando da pilha; à direita Result como união de Ok value e Err error unidos pela tag ok booleana, com if not r.ok return r e a nota de que o compilador exige tratar a falha.

À 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.

Por que isso importa. A regra do projeto (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.
3

O padrão: dependa de uma porta, não de uma concretizaçã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:

PortaO que abstraiProd vs teste
FsPortIO de filesystem (read/write/escrita-atômica)impl real node:fs · fake em memória
Clocko tempo atualrelógio do sistema · relógio fixo/avançável
backend / adapteruma chamada de modelo ou provedor de redefetch/ModelAdapter · um fake enlatado
idFactorycunhar identificadorescontador monotônico (determinístico em todo lugar)
Diagrama de um kernel central rotulado subsistema kernel cercado por cinco portas injetadas: à esquerda FsPort, Clock e idFactory em oliva; à direita WebBackend e Compressor em slate; setas convergindo ao centro; legenda mapeando as cores.

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.

o mesmo diagrama, em SVG vivo — o kernel ao centro, as portas em volta
kernel do subsistema lógica pura · retorna Result FsPort Clock idFactory WebBackend Compressor o kernel não importa fs, nem clock, nem SDK — só as portas acima
Dica de leitura Sempre que abrir um arquivo da fusão, procure o construtor (ou as 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.
4

A mesma forma, cinco vezes


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.

determinismo

FsPort + Clock — o usage store do curador

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 fixo
kernel retorna Result FsPort Clock idFactory WebBackend Compressor

FsPort + Clock — o usage store do curador

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

OperaçãoO que acontece se falharPor quê
Leitura de um sidecar corrompidook(vazio)telemetria de hot-path não pode derrubar o host
Escrita atômica que falhaerrperder 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.

a assimetria deliberada do UsageStore LEITURA best-effort sidecar corrompido / ausente ⇒ ok(vazio) — host nunca quebra ESCRITA atômica writeFileAtomic falha ⇒ err — erro real, visível
Nem toda falha é igual: perder telemetria é tolerável (ok vazio), perder uma escrita é fatal (err). A porta codifica essa decisão.

Uma porta de backend — busca/extração web

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".

Um idFactory — o gateway de clarify

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.

o gateway bloqueante sem thread bloqueante ask(question) cunha id: clarify-1 registra pendente + arma setTimeout resposta a tempo ⇒ ok(answer) timeout ⇒ entrada cai, err a promise sempre resolve — nunca pendura, nunca lança. O id determinístico torna o teste do timeout reproduzível.
O padrão promise + resolver + timeout: a espera "bloqueante" é simulada por uma promise que sempre resolve — em 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.
Exemplo guiado — uma chamada ao gateway de clarify, do id ao err
1
O kernel chama ask(question). O gateway cunha um id pelo factory injetado: monotonicIdFactory() devolve clarify-1 (determinístico — não Math.random()).
2
Ele registra uma entrada pendente sob clarify-1 num mapa de resolvers e arma um setTimeout. Retorna a promise aguardada — o kernel fica esperando, sem travar a thread.
3
Ninguém responde a tempo. O setTimeout dispara: a entrada clarify-1 é removida e a promise resolve para err(...). Nada lança, nada fica pendurado.
4
Agora você: num teste, você injeta um 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.)
5

Zod na fronteira — toda entrada não confiá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.

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.

a fronteira: nada não-validado entra dado externo não confiável safeParse o porteiro success ⇒ segue como ok !success ⇒ err (falha fechada)
O safeParse é o porteiro: válido entra como ok, inválido vira err. Nunca um throw, nunca um cast cego.
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.
Cuidado O perigoso aqui é confiar na saída do seu próprio modelo. "Veio do modelo que eu configurei, então é seguro" é uma armadilha: o modelo é uma fonte externa não-determinística como qualquer outra. Por isso a proposta dele é safeParse'd antes de qualquer escrita — nunca um as que finge confiança que não existe.
6

O retorno — por que dar todo esse trabalho


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.

Os três payoffs, em uma respiração
  • Testabilidade: injete um 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().
  • Determinismo: as mesmas entradas sempre produzem as mesmas saídas (a regra de plano do motor proíbe Date.now()/Math.random()), então as runs são reproduzíveis e o replay bate.
  • Agnosticismo de store/adapter (ADR-0009): o mesmo kernel dirige uma API de fronteira, um modelo local ou um fake offline — e o Validador real do coda pode substituir o portão padrão por injeção, sem mudar o kernel.
uma disciplina → três payoffs portas injetadas + Result, nunca throw testabilidade — fakes, zero IO, zero flaky determinismo — replay bate, sem relógio/RNG agnóstico de store — API, local ou fake (ADR-0009)
Uma decisão de design no kernel ramifica em três propriedades que você sente meses depois, na hora de testar, depurar e trocar de backend.
Lembrete: o pilar de determinismo (Clock + idFactory) e o pilar de erro (Result + Zod) não são duas disciplinas — são duas metades da mesma aposta: que um sistema previsível vale mais do que um sistema esperto.
7

Confusões comuns


Duas objeções aparecem sempre que alguém vê esse padrão pela primeira vez. Ambas têm uma resposta curta e firme.

"Portas são só interfaces — over-engineering." Em linguagem de gente: sem a tomada, você teria que abrir o abajur e soldar um fio novo toda vez que mudasse de fonte de energia. A tomada parece um exagero até a primeira vez que você precisa ligar num gerador.
"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.
"Portas são só interfaces — over-engineering." Elas são load-bearing aqui: o mesmo kernel deve rodar contra uma API real em produção e um fake num teste sem rede. Sem a costura você não consegue testar a lógica isolada, e não consegue trocar o portão padrão pelo Validador do coda sem reescrever o kernel.
"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.
8

Recapitulando


Passe pelo deck para fixar a sequência, vire os cartões para praticar recuperação, e leve as dez ideias.

A ideia central

Uma disciplina, sete subsistemas

Os subsistemas da fusão se parecem porque seguem a mesma checklist: receba portas, retorne Result, injete o não-determinístico, valide na fronteira.

O contrato

Result<T, Error>

Duas caixas, Ok e Err, separadas pela tag ok. A falha é um valor que o compilador obriga você a tratar — não uma exceção que escapa.

O padrão

Dependa de uma porta

O kernel declara do que precisa e o chamador fornece. FsPort, Clock, idFactory, backend — nenhum é construído dentro do kernel.

A metade do determinismo

Clock = idFactory

São a mesma ideia: trocar um global proibido (Date.now(), Math.random()) por uma costura injetada, para que o replay bata e o teste seja reproduzível.

A fronteira

Zod barra o intruso

Todo dado externo — proposta de modelo, JSON de backend — passa por safeParse na entrada. Inválido vira err; o passo falha fechado, sem lixo entrar.

O retorno

Três payoffs

Testabilidade (fakes, zero IO), determinismo (replay bate) e agnosticismo de store (ADR-0009: API, local ou fake, sem mudar o kernel).

1 / 6setas

Cartões de memória

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.

O contrato
O que é um Result<T, Error> e qual é a regra de ouro?
clique para virar
Uma união Ok<T> | Err<E> com a tag ok. Regra: código de biblioteca retorna Result e nunca lança.
A borda
Onde fica o único try/catch do sistema?
clique para virar
Dentro de tryCatchAsync, na borda — ele converte um throw em err. Comentário: "Never rejects."
As portas
Quais são as quatro portas que recorrem na fusão?
clique para virar
FsPort (IO), Clock (tempo), idFactory (ids) e um backend/adapter (modelo/rede). Todas injetadas.
O determinismo
Por que Clock e idFactory são a mesma ideia?
clique para virar
Ambos trocam um global proibido (Date.now()/Math.random()) por uma costura injetada — para o replay bater e o teste ser reproduzível.
A assimetria
No UsageStore, leitura corrompida e escrita falha retornam o quê?
clique para virar
Leitura corrompida → ok(vazio) (telemetria não derruba o host). Escrita falha → err (erro real). Assimetria deliberada.
A fronteira
O que acontece com a saída de um modelo antes de ser escrita?
clique para virar
É safeParse'd por Zod na fronteira (reviewProposalSchema). Forma inválida → err, falha fechada. Nunca um as cego.
As Dez ideias para levar desta lição
  1. Os sete subsistemas da fusão se parecem porque seguem uma disciplina, não por acaso.
  2. Um Result<T, Error> é Ok | Err: a falha é um valor, não uma exceção que escapa.
  3. A tag ok estreita o tipo: depois de if (!r.ok) return r;, ler r.value é seguro.
  4. Regra do projeto: código de biblioteca nunca lança; o único try/catch é tryCatchAsync na borda.
  5. Uma porta é uma interface injetada: o kernel declara o que precisa, o chamador fornece.
  6. Quatro portas recorrem: FsPort, Clock, idFactory e um backend/adapter.
  7. Clock e idFactory são a mesma ideia: injetar o não-determinístico para o replay bater.
  8. Assimetria deliberada: leitura corrompida → ok(vazio); escrita falha → err.
  9. Toda entrada externa é não confiável → Zod safeParse na fronteira → inválido vira err.
  10. O retorno: testabilidade + determinismo + agnosticismo de store (ADR-0009).
9

Verifique seu entendimento


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.

1. Uma função de biblioteca encontra um erro de rede. O que o contrato nunca-lança exige que ela faça?
(b). Código de biblioteca retorna 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.
2. Por que o UsageStore recebe um Clock no construtor em vez de chamar Date.now()?
(d). Injetar o tempo significa que um teste pode fixar o "agora", avançá-lo além de um corte e afirmar a transição exata — sem dormir, sem instabilidade. O curador e o usage store compartilham o mesmo relógio para que "registrado agora" e "decidido agora" concordem. Não é velocidade (a), precisão (b) nem fuso (c).
3. Uma proposta de modelo chega ao ciclo de aprendizado. Antes de qualquer escrita, o que acontece com ela?
(c). Em produção uma proposta é saída de modelo não confiável, então é validada com Zod na fronteira (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).
Acertos: 0/3
Para levar adiante. Você acabou de aprender a gramática que todos os subsistemas da fusão falam. Na próxima lição, veja o que acontece quando essa disciplina encontra o mundo real e algo escapa: o caso do vazamento de órfãos do Vitest — um bug de teste-safety que custou horas e virou regra permanente.