Curso / Lição 05
Lição 05 · A disciplina sob tudo

A disciplina de ports-and-injection

Por que todos os sete subsistemas da fusão se parecem? Porque cada um obedece a uma única disciplina: depender de portas injetadas (FsPort, Clock, adapter, idFactory), 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 no repositório)
CLAUDE.md — "Error handling: fail-closed Result<T, Error> … avoid throwing in library code" + ADR-0009

Esta lição destila a regra do projeto e a vê encarnada, linha a linha, em packages/contracts/src/result.ts e em quatro kernels de packages/hermes/src. Cada número/trecho tem fonte (rodapé). Por que importa pra missão: é a disciplina que faz os subsistemas do Hermes plugarem no motor do Alembic sem reescrever nada.

Objetivos desta lição
  • Distinguir um Result<T, Error> de uma exceção — e explicar por que a falha vira valor no tipo.
  • Reconhecer uma porta injetada (FsPort/Clock/adapter/idFactory) e por que o kernel nunca a constrói.
  • Aplicar a regra "Zod entra na fronteira → Result sai" a qualquer entrada não confiável.
  • Justificar a disciplina pelos três retornos: testabilidade, determinismo e agnosticismo de store (ADR-0009).
0 portas
FsPort · Clock · adapter · idFactory
0 subsistemas
a mesma forma, na fusão inteira
0 throws
em código de biblioteca
ADR-0
cintura estreita / agnóstico de store
OS TRÊS FIOS DA DISCIPLINA · uma só ideia que esta lição percorre fio a fio
① Result nunca lança ② portas injetadas ③ Zod na fronteira a disciplina (ADR-0009) testável · determinística · agnóstica

01 · O contrato: um Result que nunca lança

Antes das portas, antes da injeção, existe uma peça da qual tudo depende. Um Result é um valor que é ou sucesso ou falha — nunca uma exceção. Pense numa encomenda dos correios: ou ela chega com o pacote, ou chega com um aviso explicando por que não veio. Em nenhum caso o carteiro some no meio do caminho. É de @alembic/contracts:

// 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: depois de 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ê tem de tratar, não uma exceção que desenrola a pilha.

O TIPO Result · dois caminhos, nenhum terceiro estado — o compilador trava .value até você tratar !r.ok
f(): Result<T, Error> retorna um valor, sempre r.ok ? (tag discriminada) true usa r.value tipo T garantido false trata r.error · return r throw / exceção ✗ não existe esse ramo

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."
/** Wrap an async throwing function as a Result. Never rejects. */
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)); }
};
Em uma frase: a função promete devolver ou o resultado, ou o motivo da falha — e nunca explodir no caminho. Quem chama lê uma etiqueta (ok) e segue por um de dois caminhos. Como a encomenda: pacote ou aviso, nunca um sumiço.
No detalhe: Ok<T> e Err<E> são interfaces readonly com a discriminante literal ok: true | false. A união Result<T, E = Error> faz o TypeScript estreitar (narrow) o tipo após o guard !r.ok: dentro do ramo, r.value é T; fora, é inacessível. tryCatchAsync é a única fronteira com try/catch — converte qualquer throw de fn() em err(onError(cause)), então a Promise resolve sempre, "Never rejects".
Por que isso importa. A regra do projeto (CLAUDE.md): "fail-closed Result<T, Error> … avoid throwing in library code". 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 (no duplo sentido).
EXCEÇÃO vs Result · a exceção desenrola a pilha (invisível); o Result sobe como valor por cada quadro
throw — desenrola f3() — lança aqui f2() — pulada (não trata) f1() — pulada (não trata) catch distante (ou crash) Result — sobe como valor f3() — return err(...) f2() — if(!r.ok) return r f1() — if(!r.ok) return r caller — trata r.error
Preveja antes de continuar
Quantos blocos try/catch a "metade pública" de um subsistema da fusão deveria ter, se ele segue a disciplina à risca? Chute antes de revelar.
Zero. O único try/catch de todo o stack vive dentro de tryCatchAsync, bem na borda que encapsula uma chamada que lança. Tudo acima disso devolve Result e propaga a falha como valor (if (!r.ok) return r;). Se você chutou "um por função que pode falhar", caiu na armadilha que o Result existe para eliminar: a falha não precisa de catch — ela já é parte do tipo de retorno.

02 · O padrão: dependa de uma porta, não de uma concretização

Com o contrato firme, vem a segunda metade da disciplina. 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. É como uma tomada na parede: o aparelho não tem um gerador embutido; ele declara um plugue e você liga onde quiser — na rede elétrica de verdade, ou numa bateria de teste. Quatro costuras recorrem na 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)
UM KERNEL CERCADO POR PORTAS INJETADAS · o kernel não importa fs, nem clock, nem SDK — só as portas
kernel do subsistema lógica pura · retorna Result FsPort Clock idFactory WebBackend Compressor injetadas pelo chamador — o kernel só vê interfaces
"Inversão de dependência" em uma imagem: a seta sempre aponta para dentro do kernel. O kernel não vai buscar o mundo (não faz import fs, não chama Date.now(), não instancia um SDK); o mundo é entregue a ele, já embrulhado numa interface. Trocar o mundo (real → fake) não toca uma linha do kernel.
IMPORTS CONCRETOS DENTRO DO KERNEL · quanto do mundo o kernel "conhece" — menos é melhor
acoplamentos concretos (node:fs · SDK de rede · Date.now) que o kernel importa diretamente hardcoded 3 acoplamentos node:fs · SDK · Date.now() injetado 0 — só interfaces (portas) Zero import concreto = o kernel não "conhece" o mundo; ele só declara o que precisa. É o que o torna trocável.

03 · A mesma forma, quatro vezes

A prova de que isto é disciplina, e não acaso, é repetição. O mesmo formato aparece em quatro kernels diferentes do Hermes — cada um injeta o que precisa e devolve Result.

A MESMA FORMA REPETIDA · cada subsistema = porta(s) injetada(s) → kernel puro → Result
FsPort+Clock WebBackend idFactory Compressor? curator web clarify learning kernel puro kernel puro kernel puro kernel puro Result Result Result Result

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). A assimetria é deliberada: leitura corrompida → ok(vazio), escrita falha → err.

A ASSIMETRIA DELIBERADA DO UsageStore · ler é best-effort (ok), escrever é fail-closed (err)
read() · sidecar corrompido ok(vazio) telemetria de hot-path não pode quebrar o host writeFileAtomic() falha err perder dados precisa ser visível ao chamador Mesma porta (FsPort), dois contratos: ler tolera o ruído; escrever nunca finge sucesso.

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

DUAS SAÍDAS QUE PARECEM IGUAIS, MAS NÃO SÃO · "deu errado" (err) ≠ "achou nada" (ok([]))
search(query) o que aconteceu? rede caiu err — a busca falhou, tente de novo buscou, sem hits ok([]) — resultado válido: nada encontrado Um catch ingênuo colapsaria os dois em "erro". O tipo Result obriga a porta a manter a distinção que o chamador precisa.

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.

CICLO DE VIDA DO ClarifyGateway · sem thread bloqueante — promise + resolver + timeout
ask(question) mintId() → clarify-N registra entrada pendente + arma setTimeout respondeu a tempo? SIM promise → ok(resposta) timeout descarta entrada · promise → err Nunca trava (sempre resolve) e nunca lança (resolve para err). O id vem do idFactory injetado — determinístico.
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.

04 · A fronteira: Zod entra → Result sai

Há um terceiro fio na disciplina, e é onde as duas metades se encontram. 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. Trate-a como a fila da imigração: ninguém entra sem ter o documento conferido. Então ela é 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.
FLUXO DA FRONTEIRA · entrada não confiável → safeParse → ok(dado tipado) ou err — nunca um throw
entrada não confiável proposta · JSON · resposta schema .safeParse() ok? success ok(dado tipado) segue p/ a lógica pura !success err(new Error(...)) — fail-closed throw / exceção ✗ nunca aqui
Lembre da fila da imigração: a validação não confia e depois corrige — ela barra na entrada. Por isso a falha é err e o dado que passa já está tipado: nada não confiável circula lá dentro.
O PERÍMETRO DE CONFIANÇA · Zod é a única porta de entrada — fora é unknown, dentro é tipado
FORA · não confiável (unknown) proposta de modelo JSON de backend resposta de plataforma Zod .safeParse DENTRO · tipado & confiável dado já validado a lógica pura só vê tipos forma inválida no portão → err (não entra) um só portão · nada contorna o perímetro

05 · Refatore na mão (passo a passo → agora você)

Você viu a disciplina em quatro kernels prontos. Agora aplique-a você: pegue uma função "ingênua" (que constrói o mundo e lança) e leve-a até a forma da fusão. Recuperar o procedimento fixa mais que reler o resultado.

Exemplo resolvido · de "ingênuo" para "disciplinado" — um leitor de telemetria
1
Encontre as dependências escondidas. A versão ingênua faz import fs from 'node:fs' e chama Date.now() lá dentro. São dois "mundos" embutidos — exatamente o que a porta remove.
2
Promova cada uma a porta no construtor. fs vira private readonly fs: FsPort; o relógio vira private readonly clock: Clock; o caminho vira um argumento sidecarPath: string — sem home global. Igual ao UsageStore (linhas 58-63).
3
Valide a entrada não confiável. Antes de usar o que veio de fora, const parsed = schema.safeParse(input); if (!parsed.success) return err(...). A fila da imigração da seção 04.
4
Troque throw por Result. Toda saída vira ok(value) ou err(error). A leitura best-effort retorna ok(vazio) num arquivo corrompido; a escrita que falha retorna err (a assimetria deliberada da seção 03).
5
Verifique a costura. Em teste, injete um FsPort em memória + um Clock fixo. Zero IO, zero Date.now() — a transição vira reproduzível.
Agora você: uma função que faz fetch de uma busca web e throw em erro de rede. Quais portas você injeta e o que ela passa a retornar? Pense antes de revelar.
Injete um WebBackend (e, se comprimir, um Compressor opcional) — nada de fetch nem SDK no kernel. A assinatura passa a ser Promise<Result<readonly WebSearchResult[], Error>>: erro de rede → err, busca vazia → ok([]). É exatamente a porta WebBackend de web/types.ts:120-140. Dica: o procedimento é sempre o mesmo — promova os mundos a portas, valide a entrada, devolva Result.

06 · Injetado vs hardcoded — o trade-off lado a lado

Toda a disciplina gira em torno de uma escolha: o kernel recebe o mundo (injetado) ou o constrói (hardcoded)? Veja o mesmo kernel nas duas formas — a tabela diz os números, o diagrama torna a diferença visível.

DimensãoHardcoded (import fs, Date.now())Injetado (FsPort, Clock)
Testar sem IO/rede✗ impossível isolar — toca disco e relógio reais✓ injeta fake + clock fixo, zero IO
Determinismo / replayDate.now()/Math.random() — a VM de plano rejeita✓ mesmas entradas → mesmas saídas
Trocar de backend✗ reescrever o kernel✓ trocar o argumento (ADR-0009)
Falha de rede✗ exceção que surpreende o chamadorerr no tipo, falha tratável
O MESMO KERNEL · hardcoded (preso ao mundo real) vs injetado (porta plugável)
HARDCODED kernel import fs · Date.now() lá dentro disco + relógio reais (preso) ✗ não testa isolado · não replay INJETADO kernel recebe FsPort · Clock (interface) impl real fake em teste ✓ troca real↔fake · replay Mesma lógica nos dois — só muda quem fornece o mundo. A porta é a diferença entre testável e preso.

07 · O raio de explosão de uma troca de backend

Aqui a disciplina vira número. Quando você troca o provedor (frontier → local → fake offline), quantos arquivos mudam? Com portas, só o ponto de composição (quem injeta) muda; o kernel fica intacto. Sem portas, cada arquivo que importou o SDK direto precisa ser reescrito. Arraste para sentir o trade-off:

arquivos alterados ao trocar de backend (menos é melhor) hardcoded 12 injetado 1 (só o ponto de composição) É o agnosticismo de store do ADR-0009 medido em arquivos: a porta isola a mudança no chamador, não no kernel.
A conta da injeção
Hardcoded, o raio de explosão = o nº de arquivos que tocaram o SDK. Injetado, é sempre 1: você troca o argumento passado ao construtor. Por isso 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.

08 · O retorno — por que dar todo esse trabalho (ADR-0009)

As três disciplinas — Result, portas, Zod-na-fronteira — não são três regras soltas. São uma só ideia vista de três ângulos, e juntas pagam três dividendos:

O retorno — por que bother com tudo isso
  • 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.
  • 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 RETORNOS · as três regras convergem em testável + determinístico + agnóstico
Result nunca lança portas injetadas Zod na fronteira a disciplina ADR-0009 testável (zero IO) determinístico (replay) agnóstico de store
UM KERNEL, TRÊS MUNDOS (ADR-0009) · a mesma lógica dirige API de fronteira, modelo local ou fake offline
o mesmo kernel não muda 1 linha adapter: API de fronteira adapter: modelo local adapter: fake offline ($0) Result (mesmo contrato) + o Validador do coda por injeção Trocar de mundo = trocar o adapter injetado. É o agnosticismo de store medido na seção 07 (raio de explosão = 1).

Confusões comuns

"Portas são só interfaces — over-engineering." Aqui elas carregam peso: 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.

09 · Como isso se encaixa

Esta lição não descreve um subsistema — descreve o padrão que está debaixo de todos eles. Ports-and-injection não é uma peça da pipeline; é a regra que cada peça da pipeline obedece. Por isso ela não aparece como uma caixa no fluxo: ela é o chão sobre o qual o fluxo inteiro anda. Veja onde a disciplina toca a máquina:

A DISCIPLINA COMO BASE · contracts define o Result+as portas → cada subsistema (07–13) herda a forma → a cintura estreita (14) os liga ao motor → gates & replay colhem o dividendo
@alembic/contracts · upstream define Result<T,Error> + as 4 portas (a forma) os 7 subsistemas (Lições 07–13) — esta disciplina, encarnada memory · learning · curator · clarify · web · skills · media — cada um: porta injetada → kernel puro → Result a cintura estreita (Lição 14) o contrato fino por onde os subsistemas plugam no motor pipeline de gates (Lição 17) Proof/Validator por injeção, fail-closed determinismo & replay (Lição 28) Clock/idFactory injetados → run repetível ↑ tudo isto repousa sobre a MESMA disciplina: ports-and-injection (ADR-0009)
Clique para acender camada a camada — da base (contracts) ao dividendo (gates & replay).
Onde você está na metodologia. No mapa do curso, as Lições 07–13 abrem os sete subsistemas um a um; esta lição (05) é o gabarito que faz os sete terem a mesma forma. Ela fica antes deles de propósito: depois daqui, cada deep-dive é "a disciplina + a especificidade daquele kernel". A cintura estreita (Lição 14) é o outro lado da mesma moeda — as portas tornam o kernel agnóstico, a cintura é o contrato fino por onde ele pluga. Veja o trajeto inteiro no mapa interativo da metodologia (ou no hub do curso).

10 · Na prática

A disciplina não é teoria: é exatamente o que deixa você provar um subsistema sem rede, sem disco e sem relógio real. O teste injeta um FsPort fake, um Clock fixo e um backend enlatado — e o kernel roda determinístico. Rode você mesmo o pacote onde os quatro kernels desta lição vivem:

# roda só o pacote @alembic/hermes — onde vivem usage-store, web, clarify, learning
pnpm --filter @alembic/hermes test
# …
# ✓ src/curator/curator.test.ts   (FsPort fake + Clock fixo → zero IO)
# ✓ src/web/web.test.ts           (WebBackend fake → err ≠ ok([]))
# ✓ src/clarify/clarify.test.ts   (idFactory monotônico → ids determinísticos)
# ✓ src/learning/review.test.ts   (proposta safeParse'd na fronteira)
# Test Files  N passed
# Tests       N passed   ← todos com portas FALSAS, nada toca o mundo

A contagem exata de casos varia conforme o pacote evolui — o que importa é que passa sem tocar disco, rede ou relógio. É a disciplina virando prova. [uncertain] o número (N) de casos depende da versão do repo; o conjunto real aparece quando você roda o comando acima.

E a mesma disciplina é o que mantém o monorepo inteiro verde — a baseline que todo change precisa passar (CLAUDE.md):

# a baseline de build/test do projeto inteiro (CLAUDE.md)
pnpm -r typecheck && pnpm -r build && pnpm -w test
# typecheck: o compilador exige que você trate !r.ok antes de ler r.value
# test:      kernels rodam contra portas fake — determinístico, sem rede
Experimente · troque uma porta real por uma fake num teste (e veja zero IO)
1
Entre no repo e no pacote. cd na raiz do monorepo e rode pnpm --filter @alembic/hermes test. Repare: ele termina em segundos, sem abrir rede nem escrever no seu disco real — porque os testes injetam portas falsas.
2
Abra um teste de kernel. Em packages/hermes/src/curator/curator.test.ts (que exercita o UsageStore de usage-store.ts), procure onde o store é construído: ele recebe um FsPort em memória e um Clock fixo — não node:fs, não Date.now(). É a injeção da seção 02, no teste.
3
Faça o experimento mental (a prova da costura). Troque, na sua cabeça, o Clock fixo por outro fixo num instante diferente, ou o FsPort fake por um que devolve um sidecar corrompido. Pergunte: a saída muda de forma previsível? O kernel ainda devolve Result (e nunca lança)? Se sim, a porta está fazendo seu trabalho.
4
Confirme a baseline. Rode pnpm -r typecheck && pnpm -r build && pnpm -w test na raiz. O typecheck é a metade silenciosa da disciplina: ele recusa compilar se você ler r.value sem ter tratado !r.ok antes.
O que você acabou de provar: um kernel que depende só de portas roda idêntico com o mundo real ou com um fake. É testabilidade + determinismo (ADR-0009) que você consegue executar, não só ler. Quer ir além?
A mesma disciplina sustenta o motor: alembic run --goal GOAL.md --plan alembic.plan.ts --yes executa uma missão cujos kernels seguem esta forma, e alembic run --goal GOAL.md --plan alembic.plan.ts --coordinated --yes liga o Validador adicional por injeção (offline, só registra) — a "troca do portão padrão" da seção 06, na linha de comando. Para fechar o ciclo determinístico, alembic replay <run-id> re-executa do events.jsonl + cache: só repete porque o Clock/idFactory são injetados (Lição 28). [uncertain] esses comandos exercitam o motor inteiro, não isolam este kernel; para isolar a disciplina, o caminho direto é o pnpm --filter @alembic/hermes test acima.

Fixe os conceitos (flashcards)

Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.

Contrato
O que é um Result<T, Error>?
clique pra virar ↻
Resposta
Uma união discriminada Ok<T> | Err<E>. A falha é um valor (err), nunca uma exceção. Após !r.ok o compilador libera r.value.
Porta
Por que injetar FsPort/Clock em vez de construir?
clique pra virar ↻
Resposta
Para trocar real↔fake sem tocar o kernel: testar sem IO, fixar o tempo (replay) e ser agnóstico de store (ADR-0009).
Fronteira
O que acontece com uma entrada não confiável?
clique pra virar ↻
Resposta
schema.safeParse(input) na fronteira; forma inválida → err, fail-closed. Nada não confiável circula tipado lá dentro.
Determinismo
Por que idFactory e Clock são o mesmo padrão?
clique pra virar ↻
Resposta
Ambos substituem um global proibido (Math.random()/Date.now(), rejeitados pela VM de plano) por uma costura injetada — testabilidade + replay.

Revisão cumulativa — recupere de memória

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

1. Uma função de biblioteca encontra um erro de rede. O que o contrato nunca-lança exige que ela faça?
Correto: b — código de biblioteca retorna Result e nunca lança; o chamador trata a falha como um valor que o tipo o força a considerar. a reintroduz a exceção que o Result existe para eliminar — lançar é justo o que a regra do CLAUDE.md proíbe. c apaga a informação do erro e ainda mente no tipo (ok(null) diz "deu certo"). d trava o host num laço infinito. O único try/catch legítimo vive em tryCatchAsync, na borda, convertendo um throw em err.
2. Por que o UsageStore recebe um Clock no construtor em vez de chamar Date.now()?
Correto: d — injetar o tempo deixa um teste fixar o "agora", avançá-lo além de um corte e afirmar a transição exata; o curador e o usage store compartilham o mesmo relógio para que "registrado agora" e "decidido agora" concordem. a inventa um problema de performance que não existe. b confunde precisão com determinismo — o ponto não é resolução, é repetibilidade. c reduz a um detalhe de fuso, ignorando o replay e a proibição da VM de plano.
3. Uma proposta de modelo chega ao ciclo de aprendizado. Antes de qualquer escrita, o que acontece com ela?
Correto: c — em produção uma proposta é saída de modelo não confiável, então é validada com reviewProposalSchema.safeParse na fronteira antes de qualquer escrita; uma proposta malformada vira err. a inverte a ordem: validar na leitura já gravou lixo no disco. b confia na origem — mas "veio do modelo" é precisamente o que torna a entrada não confiável. d usa as, um cast que mente para o compilador sem checar nada em runtime — o oposto de fail-closed.
4. Você troca o provedor de busca web (frontier → local). Com a disciplina de portas, quantos arquivos mudam?
Correto: a — a porta WebBackend isola a mudança no chamador que injeta; o kernel só vê a interface e não muda. b descreve o mundo hardcoded (cada import de SDK reescrito) — exatamente o raio de explosão que a porta elimina. c é mágica: a troca é uma decisão de composição, não acontece sozinha. d erra ao reescrever o kernel — se o kernel muda numa troca de backend, é sinal de que a porta não estava lá.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Por que leitura corrompida vira ok e escrita falha vira err?", "Como injeto um Clock fixo num teste?", "Onde fica o ponto de composição que injeta as portas?". É só dizer.