Curso / Lição 08
Lição 08 · Mergulho profundo · subsistema 2 de 7

reviewAndLearn — o passe gated

Como um turno finalizado ensina o próximo — sem virar terra de ninguém. Um revisor propõe escritas duráveis de memória; um gate Validador dispõe; só as aprovadas sedimentam no MemoryStore. A restrição-chave — tirada direto da matriz de fusão e do ADR-0018 — é que o aprendizado é gated, não auto-aplicado. É um ADAPT do fork de background-review do Hermes para o estilo de portas do motor: sem thread de daemon, apenas três portas injetadas.

Leia primeiro (fonte de verdade — no repositório)
packages/hermes/src/learning/{review,gate,types}.ts + review.test.ts

Esta lição destila esses arquivos lidos literalmente (rodapé com linhas exatas) mais a proveniência em docs/alembic-hermes-fusion-matrix.md §3, docs/hermes-complete-map.md §1.10/§5.1 e os ADRs. Por que importa pra missão: é a peça que torna o Alembic um motor que aprende sozinho sem que o modelo escreva o que quiser na memória.

Objetivos desta lição
  • Explicar a forma propor → gate → aplicar e por que aprendizado é gated, não auto-aplicado.
  • Distinguir os três baldes — applied, rejected, failed — e por que failed ≠ rejected.
  • Ler o controle de fluxo fail-closed: o que continua o passe e o que aborta o passe inteiro.
  • Aplicar o gate padrão scoreThresholdGate(0.7) e a fronteira inclusiva (0.69 vs 0.70).
0.7
piso padrão (inclusivo)
3
portas injetadas · 3 baldes
[0,1]
score validado na fronteira
0
throws atravessam a fronteira

01 · O que é, afinal, um "passe gated"

Imagine um estagiário que, ao fim de cada tarefa, escreve bilhetes do que aprendeu para o caderno permanente da equipe. Se ele pudesse colar qualquer bilhete direto no caderno, em pouco tempo o caderno estaria cheio de palpites errados e ruído. Então existe um editor: o estagiário propõe o bilhete, o editor aprova ou recusa, e só o aprovado entra no caderno. Esse editor é o gate; o caderno é o MemoryStore; o passe inteiro é o reviewAndLearn.

A palavra-chave é "gated". "Gated" = com portão. O modelo nunca escreve direto na memória; toda escrita passa por um portão que pode dizer não. É o oposto de "auto-aplicado", onde o que o modelo sugere vira verdade automaticamente. O ADR-0018 fixa isso como invariante: aprendizado é gated, não auto-aplicado.

Por que "fechar" quando dá erro? (fail-closed vs fail-open)

Há duas posturas possíveis diante de um erro de infraestrutura no meio do aprendizado. O Alembic escolhe a conservadora — fail-closed: na dúvida, não aprenda. Compare:

FAIL-CLOSED vs FAIL-OPEN · o que fazer quando o gate (ou a forma) quebra
fail-OPEN (rejeitado ✗) gate quebra → "ignora o juiz e escreve mesmo assim" memória poluída sem revisão o pior caso: aprender lixo silenciosamente fail-CLOSED (o do Alembic ✓) gate quebra → return err, para o passe memória intacta · erro visível não aprender > aprender errado É a mesma postura "fail-closed" das quatro invariantes do motor (Lição 16) aplicada ao passe de aprendizado.
A IDEIA EM UMA FIGURA · propor (modelo) → dispor (gate) → sedimentar (store)
revisor (modelo) propõe bilhetes gate (editor) aprova ou recusa MemoryStore só o aprovado sedimenta "propose ⇒ dispose ⇒ sediment" — exatamente a forma descrita no topo de review.ts (linhas 1–16).

02 · A forma: propor → gate → aplicar — com os três destinos

Agora a versão precisa. Cada proposta percorre o mesmo trilho, e termina em um de três baldes. O losango no gate é a decisão central: aprovou? Repare que a recusa do gate e a falha do store são caminhos diferentes — é o que a seção 05 vai destrinchar.

FLUXOGRAMA · propor → gate → aplicar · o losango decide; três setas para três baldes
proposer(summary) chamada de modelo (prod) reviewProposalSchema .safeParse(raw) gate(proposal) approved? NÃO rejected[] gate disse não SIM memory.apply(...) store aceita? ok applied[] gate sim + store sim failed[] store disse não err do store →

Guarde a leitura de cima: há um único ponto de decisão por proposta (o losango do gate) e dois jeitos de "não dar certo" depois dele — o gate recusar (→ rejected) ou o store recusar a escrita já aprovada (→ failed). Tudo o mais é o trilho.

03 · O driver pequeno e total

O reviewAndLearn é o orquestrador do passe. Ele é deliberadamente pequeno e total (nunca lança): um summary vazio ou zero propostas faz curto-circuito para um resultado vazio — um passe no-op é válido, espelhando o "Nothing to save." da fonte. Já um erro do proposer ou do gate falha o passe inteiro fechado (err):

// packages/hermes/src/learning/review.ts:54-70
export const reviewAndLearn = async (
  summary: string, deps: ReviewDeps,
): Promise<Result<LearnOutcome, Error>> => {
  if (summary.trim().length === 0) return ok(emptyOutcome());
  const proposed = await deps.proposer(summary);
  if (!proposed.ok) return proposed;                // erro do proposer ⇒ falha fechado
  if (proposed.value.length === 0) return ok(emptyOutcome());
  const acc: OutcomeAcc = { applied: [], rejected: [], failed: [] };
  for (const raw of proposed.value) {
    const stepErr = await processOne(raw, deps, acc);
    if (stepErr) return stepErr;                    // erro do gate / forma ruim ⇒ falha fechado
  }
  return ok({ applied: acc.applied, rejected: acc.rejected, failed: acc.failed });
};

As três portas injetadas vêm num único objeto ReviewDeps — nenhum adapter concreto, nenhuma construção de store dentro do módulo (é o kernel portável, ADR-0009):

// packages/hermes/src/learning/review.ts:29-37
export interface ReviewDeps {
  readonly proposer: ReviewProposer;  // propõe escritas a partir do summary (prod: chamada de modelo)
  readonly gate: ReviewGate;          // decide se cada proposta pode sedimentar (o Validador)
  readonly memory: MemoryStore;       // store durável onde aprovadas são aplicadas (dedup reusado)
}

As duas primeiras portas são tipos de função — assinaturas, não classes. Cada uma recebe uma entrada e devolve um Result (nunca lança). Ver a forma da assinatura deixa claro o que é injetável e por quê:

AS DUAS PORTAS DE FUNÇÃO · entrada → Promise<Result<...>> (types.ts:118-130)
ReviewProposer summary: string Promise<Result<readonly ReviewProposal[], Error>> prod: 1 chamada de ModelAdapter; ModelRunFailure vira err ReviewGate proposal Promise<Result<ReviewVerdict, Error>> aqui o coda Validator (ADR-0006) pluga seu próprio gate
Preveja antes de continuar
O proposer retorna err (a chamada de modelo falhou) depois de o gate já ter aprovado e o store já ter escrito 2 propostas anteriores. O que reviewAndLearn devolve?
Pegadinha de ordem. O proposer roda uma vez só, no começo (linha 60), antes de qualquer gate ou escrita. Se ele falha, a linha 61 retorna err imediatamente — nenhuma proposta foi sequer olhada, nenhum store foi tocado. Não existe "2 escritas anteriores": o erro do proposer é a primeira coisa que pode dar errado. Lição: propor é um passo único e atômico; o loop só começa depois que há uma lista de propostas em mãos.

04 · Os três baldes

O resultado (LearnOutcome) separa três destinos, cada um com sentido próprio. Pense neles como três caixas no fim da esteira:

OS TRÊS BALDES · de onde vem cada um e o que significa
applied[] gate: SIM ✓ store: SIM ✓ a escrita sedimentou. ReviewProposal (nu) rejected[] gate: NÃO ✗ (store nem foi chamado) recusa de POLÍTICA. { proposal, reason } failed[] gate: SIM ✓ store: NÃO ✗ falha de INFRA (ex.: orçamento). { proposal, reason } applied guarda a proposta nua; rejected e failed guardam { proposal, reason } — nenhuma recusa é silenciosa.

O tipo deixa isso explícito — e os comentários do código são a melhor legenda:

// packages/hermes/src/learning/types.ts:99-106
export interface LearnOutcome {
  /** Propostas aprovadas pelo gate E escritas com sucesso no store. */
  readonly applied: readonly ReviewProposal[];
  /** Propostas que o gate recusou, cada uma com a razão do gate. */
  readonly rejected: readonly RejectedProposal[];
  /** Propostas que o gate aprovou mas o store rejeitou (ex.: char-budget). */
  readonly failed: readonly RejectedProposal[];
}

Trace passo a passo: um lote de 3 propostas

Veja o reviewAndLearn rodar sobre um lote concreto de três propostas — uma por balde. O for processa em ordem, e cada uma cai no seu balde sem afetar as outras (porque nenhuma é erro):

SEQUÊNCIA · 3 propostas, 3 baldes — a ordem é preservada e o passe completa com ok(...)
for raw of proposed.value P1 · score 0.9 · add "x" gate sim · store ok applied[] P2 · score 0.4 · add "y" gate não (0.4 < 0.7) rejected[] P3 · score 0.8 · add "z" gate sim · store err failed[] ok(LearnOutcome) 1 / 1 / 1 Nenhuma das três é ERRO, então o passe completa com ok(...) e os três baldes ficam com 1 cada.
Exemplo resolvido · processe P1 (score 0.9, add "x") pelo processOne, na mão
1
Valide a forma. reviewProposalSchema.safeParse(raw). target='memory', op={action:'add',content:'x'}, score=0.9 ∈ [0,1]parsed.success é true. (Se falhasse, seria return err e o passe abortaria.)
2
Chame o gate. const verdict = await deps.gate(proposal). Com o padrão scoreThresholdGate(): 0.9 >= 0.7ok({approved:true, reason:'score 0.9 ≥ threshold 0.7'}).
3
O gate funcionou? if (!verdict.ok) return verdict; — é ok, então segue. Aprovou? verdict.value.approved é true, então não entra no ramo do rejected.
4
Aplique no store. const written = await deps.memory.apply(proposal.target, proposal.op). Store aceita → written.ok é true.
5
Balde. Pula o ramo do failed e cai em acc.applied.push(proposal); retorna undefined → o passe continua para P2.
Agora você: processe P3 (score 0.8, add "z") — mas o store está cheio (orçamento estourado, apply retorna err). Em qual balde P3 cai, e o passe aborta? Faça antes de revelar.
P3 → failed[]; o passe NÃO aborta. Passo 1 ok (forma válida). Passo 2: 0.8 >= 0.7approved:true. Passo 4: memory.apply retorna err (over budget) → o ramo if (!written.ok) faz acc.failed.push({proposal, reason: written.error.message}) e return undefined. A chave: falha de store é recusa de infra, anotada em failed e o passe segue — só um err de gate ou de safeParse abortaria. É exatamente o teste "records a store-rejected approved write in `failed`".

05 · Por que failedrejected (a distinção load-bearing)

Esta é a sutileza que carrega a observabilidade do subsistema. Tanto rejected quanto failed são "não entrou na memória" — mas por motivos opostos, e confundi-los esconderia bugs. Uma escrita acima do orçamento (infra) nunca pode ser lida como uma recusa de política (decisão do gate). O processOne separa os dois com cuidado cirúrgico:

// packages/hermes/src/learning/review.ts:78-103 — processOne
const parsed = reviewProposalSchema.safeParse(raw);   // saída de modelo NÃO confiável
if (!parsed.success) return err(new Error(`Invalid review proposal: …`));
const proposal = parsed.data;
const verdict = await deps.gate(proposal);
if (!verdict.ok) return verdict;                      // ERRO do gate ⇒ falha o passe
if (!verdict.value.approved) {
  acc.rejected.push({ proposal, reason: verdict.value.reason }); // gate disse NÃO
  return undefined;                                 // continua o passe
}
const written = await deps.memory.apply(proposal.target, proposal.op);
if (!written.ok) {
  acc.failed.push({ proposal, reason: written.error.message }); // store disse NÃO
  return undefined;                                 // ainda continua
}
acc.applied.push(proposal);
Leia o tipo de retorno. processOne retorna Result<never, Error> | undefined. undefined significa "esta proposta foi tratada — continue o passe". Um Err significa "pare o passe inteiro, fechado". Então uma única proposta sendo rejeitada ou com falha de store não aborta o lote; só um erro de infraestrutura (forma de proposta ruim, falha do gate) faz isso. O teste "records a store-rejected approved write in `failed`" prova que a primeira escrita sobrevive enquanto o estouro vai para failed.

As três maneiras de "não dar certo", lado a lado

Compare os três finais negativos. Repare: recusa (gate ou store) continua o passe e só anota num balde; erro aborta o passe inteiro. Esta tabela é o mapa de decisão do processOne:

O que aconteceuSinal técnicoBaldeO passe...
Gate recusou a propostaok({approved:false})rejected[]continua (anota razão)
Store recusou escrita aprovadaerr de memory.applyfailed[]continua (anota razão)
Gate quebrou (serviço caiu)err do gate— (nenhum)aborta fechado
Proposta malformadasafeParse falha— (nenhum)aborta fechado
RECUSA vs ERRO · o mesmo "não", dois efeitos opostos no controle de fluxo
RECUSA (decisão) gate: ok({approved:false}) store: err(apply) → balde + return undefined o passe SEGUE para a próxima proposta ERRO (máquina quebrou) gate: err(...) safeParse: !success → return err (stepErr ≠ undefined) o passe INTEIRO aborta fechado undefined = "tratei, continue" · Err = "pare tudo". É o contrato de retorno do processOne.
1. O gate retorna ok({approved:false, reason:'too weak'}) para uma proposta. Onde ela vai parar?
Correto: c. Uma recusa do gate é ok({approved:false})rejected[], e o processOne retorna undefined, então o passe segue. a erra o balde: failed[] é só para propostas que o gate aprovou mas o store não conseguiu escrever — aqui o store nem foi chamado. b confunde recusa com erro: só um err do gate abortaria o passe, e isto é ok(...). d contradiz o tipo: toda recusa carrega { proposal, reason } — não há descarte silencioso.

06 · O gate padrão: aprender só com vitórias validadas

Até o Validator Gate real do @alembic/coda (ADR-0006) plugar seu próprio ReviewGate por injeção, o padrão conservador aprova uma proposta sse score ≥ 0.7 — a codificação mecânica de "learn only from validated wins", trazida do hermes-mini-loop (§5.1). É puro e total: sempre devolve ok(verdict), nunca lança.

// packages/hermes/src/learning/gate.ts:24-36
export const scoreThresholdGate = (
  min: number = DEFAULT_REVIEW_SCORE_THRESHOLD,   // 0.7
): ReviewGate => {
  return async (proposal) => {
    const approved = proposal.score >= min;        // fronteira INCLUSIVA
    const reason = approved
      ? `score ${proposal.score} ≥ threshold ${min}`
      : `score ${proposal.score} < threshold ${min} (learn only from validated wins)`;
    return ok({ approved, reason });               // puro & total: sempre ok(verdict)
  };
};

A fronteira é inclusiva — 0.69 reprova, 0.70 passa

O operador é >=, não >. Então exatamente 0.70 aprova; qualquer coisa abaixo reprova. Visualize a reta do score com o piso em 0.7:

RETA DO SCORE [0,1] · piso inclusivo em 0.7 (0.69 ✗ · 0.70 ✓)
0.0 0.7 (piso, inclusivo) 1.0 0.69 ✗ reprova 0.70 ✓ aprova REJEITA (score < 0.7) APROVA (score ≥ 0.7)

E a reason não é decorativa: é a string que explica a decisão no outcome (observabilidade). O ramo aprova/recusa escolhe qual template de razão é interpolado:

DECISÃO → RAZÃO · qual string o gate interpola conforme approved (gate.ts:31-33)
approved? score >= min true false `score ${score} ≥ threshold ${min}` `score ${score} < threshold ${min} (learn only from validated wins)` verdict.approved
Preveja antes de revelar
Uma proposta pontua exatamente 0.70 contra o scoreThresholdGate() padrão. Aprova ou reprova? E qual a razão registrada?
Aprova. approved = 0.70 >= 0.7 é true (a fronteira é inclusiva). A razão registrada é "score 0.7 ≥ threshold 0.7". Se fosse 0.69, reprovaria com "score 0.69 < threshold 0.7 (learn only from validated wins)". O teste "rejects 0.69 and approves 0.70 at the default 0.7 floor" fixa exatamente essa fronteira.
2. Uma proposta pontua 0.69 contra o scoreThresholdGate() padrão. O resultado?
Correto: b. approved = score >= min com min = 0.7; 0.69 está abaixo do piso inclusivo, então reprova com a razão "learn only from validated wins". a inventa arredondamento — a comparação é numérica exata, sem round. c viola a invariante "puro & total": o gate sempre retorna ok(verdict), nunca lança. d não existe: o verdict só tem approved + reason; não há estado "arriscada". Exatamente 0.70 aprovaria.

07 · A sutileza: o veredito é dado, não o Result

Aqui mora a peça mais fácil de errar — e a mais importante. Há dois eixos independentes no retorno do gate. O envelope Result (ok vs err) diz se o gate funcionou. O dado dentro dele (verdict.approved = true/false) carrega a decisão. Uma rejeição é ok({approved:false})não err.

DOIS EIXOS · "o gate funcionou?" (Result) vs "o gate aprovou?" (verdict) — matriz 2×2
eixo Result (o gate funcionou?) eixo verdict ok(...) ✓ funcionou err(...) ✗ quebrou approved: true approved: false → memory.apply applied (se store ok) ABORTA o passe verdict nem é lido → rejected[] recusa de política · passe segue ABORTA o passe err vence sempre Quando o Result é err, o verdict nem chega a ser inspecionado — a linha `if (!verdict.ok) return verdict;` corta antes.
Por que essa separação existe

É exatamente por isso que reviewAndLearn consegue distinguir "a política recusou isto" (uma rejeição legítima, esperada, anotada em rejected) de "a máquina da política quebrou" (o serviço do Validador caiu — um err que para tudo, fechado). Se rejeição e erro compartilhassem o canal do Result, um Validador fora do ar pareceria "rejeitou tudo" e o passe seguiria gravando vazio em silêncio. O teste "returns err when the gate errors (and stops the pass closed)" prova o lado do erro.

08 · A máquina de estados de uma proposta (interativa)

Junte tudo numa simulação. Escolha um cenário abaixo: a figura acende o caminho que aquela proposta percorre e os baldes mostram onde ela cai (ou se o passe inteiro aborta). É o mesmo controle de fluxo do processOne, vivo.

safeParsevalida forma gateapproved? applystore ok? applied[] rejected[] failed[] return err — ABORTA o passe
caminho ativo (sucesso)caminho ativo (recusa/erro)failed (infra)
Escolha um cenário acima.
Repare no cenário "gate quebra". O verdict nem chega a ser lido: a linha if (!verdict.ok) return verdict; corta antes de olhar approved. É a matriz 2×2 da seção 07 em movimento — err vence o eixo do verdict toda vez.

09 · Reforçar, não duplicar

O loop não adiciona nenhuma lógica de dedup própria. Escritas aprovadas fluem pelo dedup já existente do MemoryStore (Lição 07): re-propor uma entrada que já existe é um no-op de sucesso — então é contado como applied, mas o store permanece em uma entrada. Isso espelha a intenção do ON CONFLICT DO UPDATE do mini-loop: reforçar, não empilhar duplicatas.

REFORÇAR ≠ DUPLICAR · re-propor o mesmo fato conta como applied, mas o store não cresce
SE tivesse dedup próprio (❌) "x" (1ª vez) "x" (2ª vez) store com 2 entradas "x" duas políticas de dedup divergem reusando o dedup do store (✓) "x" (1ª vez) "x" (2ª vez) store com 1 entrada "x" · 2ª = applied (no-op) uma só fonte de verdade para dedup Provado pelo teste "re-proposing an existing entry does not create a second entry".

O schema da proposta valida a saída do modelo na fronteira, antes de qualquer gate ou escrita — incluindo score limitado a [0,1] e o alvo restrito a 'memory' ou 'user'. Um score: 1.5 de um modelo mal-comportado é rejeitado pelo safeParse (e aborta o passe, não vira rejeição de política):

// packages/hermes/src/learning/types.ts:62-71
export const reviewProposalSchema = z.object({
  target: memoryTargetSchema,            // 'memory' (notas do agente) ou 'user' (fatos do usuário)
  op: memoryOpSchema,                     // add / replace / remove
  rationale: z.string(),                  // por que vale lembrar — vai pro outcome (observabilidade)
  score: z.number().min(0).max(1),      // confiança do revisor; o gate compara com o piso
});

O op é uma união discriminada por action: cada operação tem campos distintos, e o schema só aceita uma das três formas. É o que o safeParse verifica antes de o gate sequer rodar:

memoryOpSchema · união discriminada por "action" (types.ts:48-52)
discriminatedUnion('action') só UMA das três formas é válida action: 'add' content: string action: 'replace' oldText + content: string action: 'remove' oldText: string
3. Por que o loop de aprendizado reusa o dedup do MemoryStore em vez de adicionar o seu próprio?
Correto: d. Roteando escritas aprovadas pelo dedup existente do store mantém uma política de dedup; re-propor uma entrada existente tem sucesso (contado applied) mas não cresce o store. a e b são racionalizações superficiais — o motivo real é evitar duas políticas divergentes, não preguiça nem impossibilidade. c contradiz o código: processOne só chama memory.apply e confia no dedup do store. Provado pelo teste "re-proposing an existing entry does not create a second entry".

10 · ADAPT, não port literal — Hermes vs Alembic

Por que "ADAPT" e não "CLONE"? Porque a disciplina foi copiada, mas o encanamento foi remodelado para o motor. O Hermes forka uma thread de background (um daemon Python no runtime AIAgent); o Alembic não tem esse runtime para forkar, então a mesma forma propor→gate→aplicar virou três portas injetadas. Compare lado a lado:

A ideia, em uma frase: os dois sistemas fazem a mesma coisa — "ao terminar, proponha o que aprendeu; um juiz aprova; só o aprovado vira permanente". O Hermes faz isso numa thread que roda em segundo plano; o Alembic faz isso numa função pequena que recebe suas três peças de fora. Mesma receita, cozinha diferente.
PeçaHermes (origem)Alembic (ADAPT)
Gatilhofork de thread de backgroundchamada de função reviewAndLearn(summary, deps)
Propordaemon no runtime AIAgent (Python)porta ReviewProposer (1 chamada de ModelAdapter em prod)
Disporfiltro score >= 0.7 do mini-loopporta ReviewGate (padrão scoreThresholdGate(0.7))
SedimentarON CONFLICT DO UPDATE (SQL)dedup do MemoryStore reusado
Dependênciasconcretas no runtimeinjetadas; nenhum adapter/store concreto importado (ADR-0009)

Quem governa cada propriedade — o mapa de ADRs

Cada decisão deste kernel rastreia até uma fonte de verdade no repositório. Este mapa liga propriedade → documento que a fixa, para você não confiar em memória:

PROVENIÊNCIA · cada propriedade do reviewAndLearn → o ADR/mapa que a governa
gated, não auto-aplica portas injetadas (sem concreto) piso 0.7 · reforçar≠duplicar gate plugável depois ADR-0018 + matriz §3a pedra angular (keystone) ADR-0009adapter/store-agnóstico hermes-complete-map §5.1o win-filter do mini-loop ADR-0006Validator Gate (coda) Nenhuma propriedade é "porque sim": cada uma aponta para um documento versionado no repositório.

Confusões comuns

"Ele auto-aplica o que o modelo propõe." O oposto — é gated pelo Validador por design (ADR-0018). O modelo só propõe; um gate decide. O gate padrão é conservador (score ≥ 0.7), e o Validador real do coda (ADR-0006) pode substituí-lo por injeção sem tocar neste kernel.
"Sem daemon significa um mecanismo diferente." É um ADAPT, não um port literal: a mesma forma propor→gate→aplicar é remodelada em portas injetadas (ReviewProposer, ReviewGate, MemoryStore). A disciplina é idêntica; o encanamento serve ao motor.

11 · Como isso se encaixa

Até aqui você olhou o reviewAndLearn por dentro. Agora afaste a câmera: onde esse passe vive na esteira do motor? A resposta curta — ele é o último elo do loop fechado, o que faz a run de hoje deixar uma marca durável para a run de amanhã. Ele só roda depois que todos os gates passaram, e o que ele produz é lido de volta no começo do próximo turno como contexto.

FLUXO · onde o passe gated entra no loop fechado do motor (clique “Passo a passo” para acender)
run (unidades)o turno executa Proof + Validadorgates passam (Lição 17) só se --learn ESTA LIÇÃO · reviewAndLearn (o passe gated) proporrevisor ReviewGatedispõe aplicarsó aprovado applied / rejected / failed — os três baldes da seção 04 gate quebra ⇒ falha fechado, memória intacta MemoryStore (Lição 07)MEMORY.md + USER.md no run-dir fecha o loop: vira contexto do próximo turno
Clique “Passo a passo” para acender o caminho da run até o MemoryStore, um elo por vez.

Onde você está na metodologia. O motor do Alembic é um loop: aprender → analisar → executar uma unidade → verificar na fronteira real → decidir, e então recomeçar com o que se aprendeu. O reviewAndLearn é a peça que torna esse “recomeçar com o que se aprendeu” literal e seguro: sem ele, cada run começaria do zero; com ele — e só com o gate no caminho — a memória cresce apenas com vitórias validadas. Para ver a esteira inteira em movimento, abra o mapa interativo da metodologia.

CONECTA COM · as quatro lições vizinhas deste elo
reviewAndLearn Lição 08 (você está aqui) Lição 04 · loop Lição 07 · memory Lição 17 · gates Lição 23 · lab

12 · Na prática

Chega de teoria — rode o passe. O aprendizado é opt-in: uma run normal não aprende. Você liga o passe gated com a flag --learn, e roda tudo hermético ($0, sem rede) com --offline. Nesse modo, o proposer é o adapter offline determinístico e o gate é o Validador — exatamente o caminho que você acabou de estudar:

# liga o passe gated (ADR-0018) ao fim de uma run, hermético e $0
alembic run --goal GOAL.md --plan alembic.plan.ts --learn --offline --yes

# … a run executa as unidades e passa pelos gates (Proof + Validador) …
# então, SÓ porque --learn está ligado, vem a última linha do passe:
#   learning gate: 2 applied, 1 rejected
De onde sai essa linha. É o deps.write no fim do Learning Gate em apps/cli/src/commands.ts: imprime learning gate: ${applied} applied, ${rejected} rejected. Os números vêm direto dos baldes do LearnOutcome (seção 04). O balde failed não entra nessa linha-resumo — ele fica no log estruturado run:learning-pass-done (com applied/rejected/failed). No offline o resultado é determinístico — os contadores exatos dependem do que o proposer offline gera para o seu GOAL.md.

Dois detalhes que conectam direto com as seções anteriores. Primeiro: sem a flag, nada acontece — o aprendizado é aditivo e desligado por padrão, então uma run verde continua verde. Segundo: se não houver adapter de proposer disponível, o passe é pulado (log run:learning-pass-skipped), nunca quebra a run — é a mesma postura fail-closed da seção 05 aplicada ao próprio gatilho.

Experimente · rode o passe gated e leia o resultado
1
Entre no repositório e garanta o build. cd alembic e então pnpm -r typecheck && pnpm -r build && pnpm -w test — o baseline tem que estar verde antes de qualquer run.
2
Tenha um GOAL.md + alembic.plan.ts. Se ainda não tem um escopo, gere com alembic plan "<seu objetivo>" — ele materializa o GOAL.md, o contrato e o alembic.plan.ts no diretório.
3
Rode com o passe gated, hermético. alembic run --goal GOAL.md --plan alembic.plan.ts --learn --offline --yes. O --offline dispensa o gateway (sem rede, $0); o --yes auto-aprova o gate do plano (não-interativo); o --learn liga o passe.
4
Leia a última linha. Procure learning gate: N applied, N rejected na saída. applied = aprovadas pelo gate e escritas; rejected = o gate disse não (política). Se aparecer run:learning-pass-skipped, faltou um proposer — não é um erro da run.
Agora você: abra o run-dir e veja a memória sedimentada. As escritas applied caem em MEMORY.md (notas do agente) e USER.md (fatos do usuário) dentro do diretório da run — exatamente o store da Lição 07. Re-rode o mesmo escopo: re-propor um fato já existente é um no-op de sucesso (a seção 09: reforçar, não duplicar), então o store não cresce. [uncertain] o caminho exato do run-dir depende do seu --data-dir/config; use alembic runs list para localizar a run e alembic tail <run-id> para reler o log.

Quer ir além do resumo de uma linha? O passe inteiro também é exercitado pelos testes do kernel — pnpm --filter @alembic/hermes test roda os 14 casos do review.test.ts (a fronteira 0.69/0.70, failed vs rejected, fail-closed nos erros de proposer/gate) que esta lição destilou. É a prova de que o comportamento que você leu é o comportamento que roda.

Fixe os conceitos (flashcards)

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

Forma
Quais os três passos de um passe gated?
clique pra virar ↻
Resposta
Propor (revisor/modelo) → gate dispõe (aprova/recusa) → aplicar no MemoryStore. Só o aprovado sedimenta (ADR-0018).
Baldes
failed vs rejected?
clique pra virar ↻
Resposta
rejected = o gate disse não (política). failed = o gate aprovou mas o store não escreveu (infra, ex.: orçamento). Distintos de propósito.
Gate
Qual o piso padrão e ele é inclusivo?
clique pra virar ↻
Resposta
score ≥ 0.7 (DEFAULT_REVIEW_SCORE_THRESHOLD). Inclusivo: 0.70 aprova, 0.69 reprova. "Learn only from validated wins."
Result vs verdict
Rejeição é err?
clique pra virar ↻
Resposta
Não. Rejeição é ok({approved:false}). Result diz se o gate funcionou; verdict.approved diz a decisão. err só = gate quebrou → aborta.

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.

4. Num passe, a 2ª de 5 propostas tem forma inválida (safeParse falha). O que acontece com as outras?
Correto: c. Uma proposta malformada é erro de infraestrutura: processOne retorna err, o stepErr é truthy e reviewAndLearn retorna na hora (linha 67). As propostas seguintes nem são olhadas. a/b confundem erro de forma (aborta) com recusa (anota em balde e segue) — malformação não é nem rejected nem failed. d é o comportamento "tolerante" que o design rejeita: fail-closed significa parar, não ignorar.
5. O que carrega a decisão de aprovar/recusar do gate?
Correto: a. A decisão vive em verdict.approved; o envelope Result (ok/err) só diz se o gate funcionou. b inverte os eixos: tanto aprovação quanto recusa são ok(...) — o que muda é o approved lá dentro. c viola "puro & total": o gate nunca lança. d confunde o efeito (a proposta acaba em rejected) com a causa (o booleano do verdict).
6. Por que reviewAndLearn é um ADAPT do Hermes, não um CLONE literal?
Correto: b. ADAPT = mesma disciplina, encanamento remodelado: o fork de thread vira reviewAndLearn(summary, deps) com ReviewProposer/ReviewGate/MemoryStore injetados. a erra os números: o piso continua 0.7, igual ao mini-loop. c descreveria um CLONE literal — justamente o que não é, já que a estrutura mudou. d contradiz o ADR-0018: continua gated, nunca auto-aplica.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Por que o store-failure continua o passe mas o gate-error aborta?", "Como o coda Validator real plugaria seu ReviewGate?", "O que entra num summary de turno?". É só dizer.