Lição 28 · Curso de Fusão · Parte 5 · Engenharia · Determinismo e replay
Parte 5 · Engenharia · Lição 28

Determinismo e replay: o mesmo input, o mesmo run

A invariante ③ (lição 16) diz que um run é endereçado por conteúdo e reproduzível. Esta lição é o mecanismo: o id de um run é o hash do seu spec, então o mesmo spec sempre cai no mesmo diretório; esse diretório é um log append-only mais um checkpoint, então um run que caiu retoma; e o módulo de plano tem banidas as três funções que quebrariam tudo isso — Date.now(), new Date() e Math.random(). Onde tempo real e acaso real são genuinamente necessários, eles entram por um seam injetado — um Clock e uma fábrica de ids — nunca por um global. É essa única disciplina que torna possível dizer "reproduza exatamente este run".

Cada bloco tem uma versão Simples e uma Técnica. Abra a técnica quando quiser o código real.
O que presumimos de você
  • Que você viu a invariante ③ (lição 16): um run é endereçado por conteúdo e reproduzível. Aqui mostramos como isso é construído, peça por peça.
  • Que "determinístico" pode soar técnico. Não é: significa só "a mesma entrada produz sempre a mesma saída" — sem surpresas, sem depender de que horas são ou de um número sorteado.
  • Nada além disso. Você não precisa saber criptografia para entender um hash: é uma impressão digital curta de um conteúdo — muda o conteúdo, muda a impressão.
1

A grande ideia: o mesmo input, o mesmo run


Ao terminar esta lição você consegue
  • Explicar por que o id de um run é o hash do seu spec e não um timestamp ou UUID.
  • Distinguir runIdFor(spec) (a identidade do run) de freshId() (um id de correlação, descartável).
  • Recitar o layout do diretório de run: events.jsonl + checkpoint.json + meta.json.
  • Dizer quais três construções o plano tem banidas e por que elas quebrariam o replay.
  • Explicar o seam injetadoClock e fábrica de ids — como a única porta por onde o não-determinismo entra.

Imagine que você roda um trabalho hoje e, daqui a um mês, precisa rodar exatamente o mesmo — para auditar, depurar ou retomar de onde parou. Para isso ser possível, "o mesmo run" precisa ser uma coisa encontrável e repetível. O Alembic garante isso com uma ideia só, usada com disciplina: o não-determinismo é uma dependência — então é controlado, não espalhado.

Pense numa receita de bolo. Se a receita disser "asse por quanto tempo der na sua agenda hoje" e "ponha um número qualquer de ovos", dois cozinheiros nunca farão o mesmo bolo — e você nunca conseguirá reproduzir o seu. Uma receita reproduzível fixa cada passo. O módulo de plano do Alembic é a receita: ele não pode consultar o relógio nem sortear um número, porque isso tornaria o bolo diferente a cada fornada.
Infográfico de quatro estações ligadas por setas: estação 1 'spec' contendo os chips goal, plan e contract; estação 2 'runIdFor(spec)' com a nota 'canonicalJson + SHA-256, chaves ordenadas'; estação 3 um selo com o id 'run-7f3a… (16 hex)' e a etiqueta 'a ordem dos campos NÃO muda o hash'; estação 4 uma pasta 'runs/<runId>/' contendo as três linhas events.jsonl, checkpoint.json e meta.json; uma seta curva de volta rotulada 'replay / resume — relê o log' fecha o ciclo; ao pé, um quadro de contraste 'freshId() = randomUUID() — id não-determinístico, só para correlação, NUNCA para o run'.

A identidade de um run É o hash do seu spec — por isso amanhã você ainda encontra exatamente este run.

Por que isto importa? Sem determinismo, "reproduza este run" é uma promessa vazia: o mesmo pedido daria diretórios diferentes, ramos diferentes, custos diferentes. Determinismo transforma um run de um evento efêmero numa coisa endereçável — algo que você pode nomear, reencontrar e re-executar com confiança de que sairá igual.
Determinístico vs não-determinístico mesmo input f puro (sem relógio/acaso) SEMPRE a mesma saída ✓ reproduzível lê Date.now()/Math.random() saída DIFERENTE a cada run ✗ replay quebra
O motor escolhe o caminho de cima — e aplica a escolha, em vez de torcer para você lembrar dela.
2

ID por conteúdo: o run É o hash do seu spec


A identidade de um run não é um carimbo de tempo nem um UUID aleatório — é o hash do conteúdo do seu spec. A função runIdFor(spec) pega a especificação do run (goal + plan + contract) e devolve uma impressão digital. Mude qualquer campo do spec e você terá um id diferente — e, portanto, um diretório diferente.

É como o ISBN de um livro derivar do conteúdo: dois exemplares idênticos compartilham o mesmo código; troque uma vírgula da edição e é outro código. O id é o conteúdo, não a data em que você o imprimiu.
Preveja antes de continuar

Você roda o mesmo goal + plan + contract duas vezes, em dias diferentes. Os dois runs caem no mesmo diretório, ou em dois diretórios distintos?

No mesmo diretório. Como o id é runIdFor(spec) — um hash do conteúdo — e o conteúdo é idêntico, o id é idêntico. Por isso os dois podem se retomar: o segundo encontra o log do primeiro. A data não entra na conta.
avalanche — mude um caractere e o id inteiro vira outro
{goal, plan, contract: v1} {goal, plan, contract: v2} runs/run-7f3a2c91…/ (retomável) runs/run-b4e09da7…/ (run novo) uma letra de diferença ⇒ um id totalmente diferente ⇒ outro diretório

o id do run é a impressão digital do spec

Na orquestração, o runId é literalmente derivado do spec via runIdFor (que vive em ./ids.js). Não há leitura de relógio nem chamada a um gerador aleatório: o mesmo spec sempre dá o mesmo id, e qualquer mudança de campo dá um id novo.

packages/swarm/src/orchestrator.ts:168
import { runIdFor } from './ids.js';
// …
const runId = runIdFor(spec);   // impressão digital (hash) do spec
// mude qualquer campo de `spec` ⇒ runId diferente ⇒ diretório de run diferente

endereçamento por conteúdo nos stores também

A mesma ideia reforça os stores append-only: cada registro carrega o SHA-256 do seu JSON canônico (chaves ordenadas recursivamente), então "re-acrescentar conteúdo idêntico é um no-op". A idempotência é estrutural — rodar duas vezes não duplica.

packages/etl/src/stores.ts:28-29, 80-97
// - CONTENT-ADDRESSED: cada registro carrega o SHA-256 do seu JSON canônico;
//   re-acrescentar conteúdo idêntico é um no-op (dedupe por hash).
const canonicalJson = (value) => { /* chaves ordenadas recursivamente */ };
const hash = sha256Hex(canonicalJson(payload));   // a mesma estrutura ⇒ o mesmo hash
A identidade do run é derivada, não inventada spec goal plan contract runIdFor(spec) canonicalJson + SHA-256 runs/run-7f3a…/ o id nomeia o diretório a ordem dos campos não muda o hash · qualquer campo muda ⇒ run novo
O id não é atribuído de fora; ele cai do conteúdo. É por isso que "o mesmo run" é uma noção estável no tempo.
3

freshId: o id que NÃO é o do run


O motor também tem um id não-determinístico — mas ele jamais nomeia um run. O freshId() devolve um UUID aleatório, e serve só para correlação: por exemplo, marcar um requestId numa requisição do worker para você cruzar logs. É o oposto exato de runIdFor: um descartável, o outro é a identidade.

Pense na diferença entre o número de um processo (estável, definido pelo conteúdo do caso) e a senha da fila que você tira ao chegar (um número novo a cada visita, só para te chamarem). Você nunca arquivaria o processo pela senha da fila.

dois ids, dois propósitos

freshId é literalmente randomUUID() — não-determinístico de propósito — e o worker o usa para requestId (correlação de logs). A regra é: o que identifica um run deriva do conteúdo (runIdFor); o que só correlaciona pode ser aleatório (freshId). Confundir os dois quebraria a reprodutibilidade.

packages/swarm/src/ids.ts:33 · packages/swarm/src/worker.ts
import { createHash, randomUUID } from 'node:crypto';
// identidade do run: derivada do conteúdo (determinística)
export const runIdFor = (spec) => createHash('sha256') /* …spec canônico… */;
// id de correlação: aleatório (NÃO-determinístico) — nunca nomeia um run
export const freshId = (): string => randomUUID();
CuidadoO perigo não é "usar UUID aleatório nunca". É usá-lo onde a identidade precisa ser estável. freshId existe e é legítimo — para correlação. Para a identidade de um run, ele seria veneno: amanhã o run estaria perdido.
Dois ids, propósitos opostos runIdFor(spec) derivado do CONTEÚDO · SHA-256 estável no tempo · idêntico p/ spec igual → IDENTIDADE do run freshId() randomUUID() · ALEATÓRIO novo a cada chamada · não reproduzível → só CORRELAÇÃO (requestId)
O acaso não é proibido no motor — é confinado ao que pode ser aleatório (correlação) e mantido longe do que precisa ser estável (identidade).
4

O diretório determinístico


Como o id é estável, o caminho do run também é. Cada run vive em <baseDir>/runs/<runId>/ — um lugar previsível, com um log append-only de tudo que aconteceu, mais um checkpoint do último estado retomável. É esse substrato que torna o resume e o replay possíveis (lição 16).

É um diário de bordo: cada linha é acrescentada ao fim, em ordem, e nunca apagada. Se o navio para no meio da travessia, você lê o diário até a última linha e segue exatamente de onde parou — não recomeça a viagem.
ArquivoO que guardaPapel no replay
events.jsonlappend-only; todo evento, em ordema história completa para reler
checkpoint.jsono último estado retomávelde onde o resume continua
meta.jsona impressão digital de goal/plan/contractvalidada no --resume
o log só cresce; o checkpoint avança; um crash retoma da última linha
events.jsonl e1 · run.started e2 · unit.started e3 · proof.passed e4 · unit.done e5 · unit.started checkpoint → e4 crash após e5 --resume: relê e1…e5,continua do checkpoint (e4)

o caminho é uma função pura de (baseDir, runId)

O diretório é resolvido por resolveRunDir, que só junta baseDir + 'runs' + runId. Nenhuma data, nenhum acaso: dado o mesmo runId, o mesmo caminho. Os runs ficam num namespace próprio, separados de planos, cursos, skills e stores.

packages/etl/src/run-directory.ts:58-59
/** Constrói o caminho absoluto do diretório de um run. */
export const resolveRunDir = (baseDir: string, runId: string): string =>
  join(expandTilde(baseDir), 'runs', runId);
// o diretório guarda os journals append-only: events.jsonl, checkpoint.json, …
spec → id → diretório → replay (o ciclo fechado) specgoal+plan+contract runIdFor(spec)hash SHA-256 runs/<runId>/ events.jsonl (append-only) checkpoint.json meta.json (fingerprint) replay / resumerelê o log o replay relê o log e continua de onde parou
Identidade estável → caminho estável → log relegível. Tire qualquer elo e o replay deixa de fechar.
5

O banimento: nada de Date.now() no plano


Um módulo de plano (alembic.plan.ts) é a descrição determinística do que rodar. Se um plano pudesse ler o relógio ou sortear um número, duas avaliações do mesmo plano divergiriam — e o replay seria uma mentira. Por isso o motor tem três construções banidas no plano: Date.now(), new Date() e Math.random().

É como uma planta de arquitetura proibir a frase "a altura desta parede é a temperatura de hoje". A planta tem de descrever a casa de um jeito que dê a mesma casa para qualquer construtor, em qualquer dia. Medidas que mudam sozinhas não cabem numa planta.
// alembic.plan.ts — estas três disparam o erro de não-determinismo:
const id = Date.now();        // ✗ rejeitado — relógio de parede
const t  = new Date();         // ✗ rejeitado — relógio de parede
const r  = Math.random();      // ✗ rejeitado — acaso
No plano: três construções banidas Date.now()relógio de parede new Date()relógio de parede Math.random()acaso permitido em todo o resto: Clock injetado · id factory injetada
Guarde istoO banimento vale no módulo de plano, não no código de aplicação. Um comando de CLI pode ler o relógio à vontade; o plano que descreve um run não pode. A fronteira é exatamente onde o não-determinismo quebraria a reprodutibilidade.
O erro tem nome. No guia de troubleshooting do projeto: "Non-determinism error — remova Date.now(), new Date(), Math.random() do módulo de plano." O motor não confia que você vai lembrar; ele verifica e falha fechado se encontrar.

a regra está escrita em dois lugares

A doutrina está no CLAUDE.md do repositório, e é repetida no troubleshooting — para que tanto humanos quanto agentes a vejam. É a mesma postura fail-closed da lição 26: o caminho seguro é o padrão.

CLAUDE.md:36, 186
- **Determinism:** plan modules (`alembic.plan.ts`) must not use
  `Date.now()`, `new Date()` or `Math.random()`. The VM rejects them.
- **Non-determinism error** — remove `Date.now()`, `new Date()`,
  `Math.random()` from the plan module.
6

O scanner: como o banimento é aplicado


Como o banimento é aplicado? Por uma varredura do texto do plano à procura das três construções proibidas, antes de o plano rodar. Se alguma aparecer, o run falha fechado — ele não roda. A varredura é deliberadamente conservadora: prefere um falso positivo a deixar o não-determinismo entrar num run que precisa ser reproduzível.

É o detector de metais na entrada: ele apita por excesso de cautela. Apitar à toa de vez em quando é um aborrecimento; deixar passar uma faca é uma falha grave. O scanner escolhe o aborrecimento.
Como o banimento é aplicado: varredura, depois portão plan.ts (texto)só se termina em .ts varredura por regex/Date.now/ /new Date/ /Math.random/conservadora (falso+ > falso−) nenhum casa → ok, run prossegue algum casa → err, run PARA (fail-closed)

Cole mentalmente um plano no scanner abaixo e veja o veredito. Alterne entre um plano limpo e um plano com uma chamada proibida:

uma varredura por regex, fail-closed

O check é descrito como uma "verificação leve de determinismo, segura para TypeScript": uma varredura por regex pelas três construções. Não é um sandbox que executa o plano — é uma análise de texto, propositalmente conservadora ("um falso positivo é mais seguro que permitir não-determinismo num run retomável"). Se o módulo termina em .ts, o check roda; se acha algo, o carregamento do plano vira um err — o run para.

packages/vm/src/run-plan.ts:8-22, 58-63
// Verificação leve de determinismo, segura p/ TS: varre os construtos proibidos.
const forbidden = [
  { pattern: /\bDate\.now\s*\(/,      name: 'Date.now()' },
  { pattern: /\bnew\s+Date\s*\(\s*\)/, name: 'new Date()' },
  { pattern: /\bMath\.random\s*\(/,   name: 'Math.random()' },
];
// …se algum casar:
return err(`plan module ... is non-deterministic: ${determinism.reason}`);
Nota de engenhariaPor ser uma varredura de texto (e não execução), o scanner pode, em teoria, apitar para Date.now dentro de uma string ou comentário. Essa é a troca consciente: a varredura erra para o lado seguro. O custo de um falso positivo é reescrever uma linha; o custo de um falso negativo é um run que não reproduz.
7

A saída: injete o relógio e a fábrica de ids


Sistemas reais precisam de tempo real (um curator decidindo "obsoleto após 30 dias") e de ids únicos (uma pergunta de clarify precisa de um identificador). A resposta não é proibi-los — é torná-los um seam injetado: produção passa o de verdade, e um teste passa um falso. Você já viu os dois nas lições anteriores.

É como a tomada na parede: o aparelho não gera a própria eletricidade nem a esconde dentro de si — ele a recebe por um plugue. Em casa, o plugue dá energia real; na bancada de testes, dá uma fonte controlada. Mesma forma de encaixe, fonte trocável.
Infográfico de duas metades contrastantes. À esquerda, painel de PROIBIDO 'no módulo de plano (alembic.plan.ts) a VM rejeita' listando Date.now(), new Date() e Math.random() cada um com um X, e a nota 'duas avaliações divergiriam = replay viraria mentira'. Uma seta-ponte central rotulada 'a saída: vire um seam injetado' cruza para a metade direita, painel 'em todo o resto, o tempo e o id entram por um seam que você controla', com dois cartões: cartão Clock (epoch ms) com a nota 'produção passa o relógio real, o teste passa um fixo' e cartão id factory com 'monotonicIdFactory; produção passa o monotônico, o teste passa um contador'; abaixo, o selo 'controle o seam e você controla o replay'.

O não-determinismo é uma dependência: bani-lo no plano, injetá-lo em todo o resto. Uma ideia, dois chapéus.

O seam: mesma forma de encaixe, fonte trocável produção: relógio real teste: relógio fixo seam: Clock curator (idêntico)active→stale→archived o subsistema não sabe qual fonte está plugada — por isso o teste é determinístico

O Clock do curator

O curator recebe o tempo como um Clock injetado (epoch ms) — nunca Date.now()/new Date(). Um teste passa um relógio fixo e as transições active → stale → archived ficam determinísticas (lição 9).

um teto relativo em ms, lido contra o Clock injetado
lastActivityAt (antigo)→ stale lastActivityAt (recente)→ active clock() − staleAfterMs agora = clock()

tempo é um valor que entra pela porta

O Clock é só um () => number (epoch ms). O comentário do tipo é explícito sobre o porquê: o default de produção embrulha o relógio real, mas o plano não pode usar Date.now()/new Date() porque "isso quebra o replay". O curator guarda seus limiares em milissegundos justamente para que o Clock injetado seja a única fonte de tempo.

packages/hermes/src/curator/types.ts:148-154
// Uma fonte de tempo monotônica, em epoch ms. O default de produção
// embrulha o relógio real; NÃO use Date.now()/new Date() no código de
// plano — isso quebra o replay.
export type Clock = () => number;

A fábrica de ids do ClarifyGateway

O ClarifyGateway recebe uma idFactory em vez de chamar um global. Um teste passa um contador (monotonicIdFactory); produção passa o monotônico. Mesma forma, determinística no teste (lição 10).

a fábrica injetada produz ids previsíveis — não aleatórios
monotonicIdFactory('q')injetada no teste q-1 q-2 q-3 … previsível 1ª chamada com randomUUID() aqui, cada run daria ids diferentes ⇒ o teste nunca casaria

o id da pergunta também é injetado

O construtor aceita uma idFactory opcional; quando ausente, usa o monotonicIdFactory() padrão — nunca um global de acaso. No teste, passa-se monotonicIdFactory('q') e as perguntas ganham ids previsíveis (q-1, q-2, …), tornando o fluxo de clarify reproduzível.

packages/hermes/src/clarify/gateway.ts:73, 176
// default = um contador monotônico; nunca um global aleatório
this.mintId = options.idFactory ?? monotonicIdFactory();
// …
export const monotonicIdFactory = (prefix = 'clarify'): (() => ClarifyId) => { /* contador */ };
O princípio: o não-determinismo é uma dependência — então injete-o

Tempo e acaso são efeitos colaterais, exatamente como o filesystem. A segunda invariante do motor — kernel puro, efeitos colaterais injetados — se aplica a eles também. Banir os globais no plano e passar um Clock/uma idFactory em todo o resto é uma ideia usando dois chapéus: o único não-determinismo de um run entra por um seam que você controla. Controle o seam e você controla o replay.

Flashcard · vire
Por que o plano não pode chamar Date.now()?
clique para ver
Porque duas avaliações do mesmo plano dariam estruturas diferentes — e o replay deixaria de reproduzir. O plano é a descrição determinística de um run.
Flashcard · vire
Onde o curator consegue "agora" sem violar o determinismo?
clique para ver
De um Clock injetado (epoch ms). Produção passa o real; o teste passa um fixo. O global Date.now() nunca é tocado.
Flashcard · vire
Qual id é descartável e qual é a identidade?
clique para ver
freshId() (UUID aleatório) é descartável, só para correlação. runIdFor(spec) (hash do conteúdo) é a identidade do run.
8

Explorador de hash


Toque na ideia central com as mãos: dois specs entram, dois ids saem. Edite qualquer campo e veja o id mudar — e veja que campos iguais dão ids iguais, mesmo digitados em momentos diferentes. (É um hash didático e estável, só para sentir a regra; o real é SHA-256.)

Spec A
Spec B
runIdFor(A)
runIdFor(B)
RepareQuando A e B são idênticos, os ids coincidem e o veredito diz "mesmo spec ⇒ mesmo diretório (retomável)". Mude uma letra de qualquer campo e o id salta — "spec diferente ⇒ run novo". É exatamente o que runIdFor garante.
9

Por que isto é a rocha do replay


Junte as peças. Ids por conteúdo fazem com que "o mesmo run" seja encontrável. O log append-only + o checkpoint fazem com que um run possa ser relido e retomado. O banimento no plano faz com que reavaliar o plano produza a estrutura idêntica. O Clock/a fábrica de ids injetados fazem com que o não-determinismo residual seja capturado e reproduzível. Remova um só e o replay quebra.

Quatro pilares — tire um e o telhado cai REPLAY reproduzível id por conteúdo runIdFor(spec) log + checkpoint events.jsonl banimento no plano scanner regex Clock + id factory injetados sem ele: o run some amanhã sem ele: nada a reler/retomar sem ele: o plano re-roda diferente sem ele: "agora" varia no replay princípio: o único não-determinismo entra por um seam que você controla a disciplina é holística — por isso a VM aplica a parte fácil de esquecer, automaticamente
Replay não é um recurso isolado: é a propriedade emergente de quatro disciplinas que se sustentam mutuamente.
1
ideia central: não-determinismo é dependência
3
construções banidas no plano
2
seams injetados: Clock + id factory
4
pilares que o replay exige juntos
10

Exemplo guiado


Você escreveu um plano que precisa de um identificador único por unidade e de uma decisão "expira em 7 dias". Veja o caminho errado virar o caminho determinístico.

Tornando um plano não-determinístico em reproduzível
1
O reflexo errado. No plano você escreve const id = Date.now() para o identificador e const prazo = new Date() para o "agora". Parece inofensivo.
2
O run nem começa. O scanner de determinismo varre o módulo, casa /\bDate\.now\s*\(/ e /\bnew\s+Date\s*\(\s*\)/, e o carregamento do plano retorna err: "non-deterministic". Fail-closed — melhor parar agora do que gravar um run que não reproduz.
3
Mova o acaso para fora do plano. O identificador único não pertence à descrição do run. Se for correlação, é trabalho do runtime (um freshId() no worker); se for identidade, deriva do conteúdo da unidade.
4
Injete o tempo onde ele é legítimo. A decisão "expira em 7 dias" vira um limiar em ms comparado a um Clock injetado (como no curator). Produção passa o relógio real; o teste passa um fixo — e a decisão fica reproduzível.
5
Agora você tenta. Pegue este plano-fragmento e diga o que o scanner faz e como consertar: const seed = Math.random(); return planFor(seed);
Sua vez — responda antes de revelar

O que acontece com const seed = Math.random() dentro do plano, e qual é o conserto determinístico?

O scanner casa /\bMath\.random\s*\(/ e o run falha fechado — o plano nunca roda. O conserto: a "semente" não pode ser sorteada dentro da descrição do run. Ou ela deriva de algo do próprio spec (determinística por conteúdo), ou, se de fato precisa de acaso, esse acaso é um seam injetado (uma fonte de aleatoriedade passada de fora, fixada no teste) — nunca Math.random() no plano. Assim duas avaliações do mesmo plano dão a mesma estrutura, e o replay continua honesto.
11

Confusões comuns


"O banimento significa que o Alembic nunca pode usar a hora atual." Não. Ele bane os globais no módulo de plano, onde o não-determinismo quebraria o replay. Em todo o resto, o tempo entra por um Clock injetado — totalmente usável, só que controlável. Um comando de CLI lê o relógio; um plano que descreve um run não.
"Ids por conteúdo são só para deduplicação." Dedup é um benefício (re-acrescentar conteúdo idêntico é um no-op), mas o propósito mais fundo é o replay: o id é a impressão digital do spec, então resume/replay conseguem encontrar e re-executar exatamente o mesmo run. Identidade e idempotência caem do mesmo hash.
"O scanner executa o plano para verificar." Não — é uma varredura de texto por regex, não um sandbox que roda o código. É por isso que ele é conservador (pode apitar para um Date.now dentro de uma string). A troca é deliberada: errar para o lado seguro custa uma reescrita; o contrário custa um run que não reproduz.
"freshId é um bug de não-determinismo." Não. freshId() é aleatório de propósito e legítimo — para correlação (um requestId em logs). O erro só existiria se ele nomeasse a identidade de um run. A regra é: identidade deriva do conteúdo; correlação pode ser aleatória.
12

Recapitulando


A invariante ③

O mesmo input, o mesmo run

Um run é endereçado por conteúdo e reproduzível. Esta lição é o mecanismo que torna "reproduza este run" verdadeiro — não uma promessa vazia.

spec runId diretório
1
Id por conteúdo

runIdFor(spec)

A identidade do run é o hash do spec. Mesmo spec ⇒ mesmo diretório (retomável). Qualquer campo muda ⇒ run novo. freshId() é o aleatório — só correlação.

spec (conteúdo) run-7f3a… (estável)
2
O diretório

log + checkpoint + meta

events.jsonl (append-only) + checkpoint.json (estado retomável) + meta.json (fingerprint validado no --resume). O substrato do resume.

events.jsonl checkpoint.json meta.json
3
O banimento

nada de relógio no plano

No plano, banidos: Date.now(), new Date(), Math.random(). Um scanner por regex varre e falha fechado se achar — a parte fácil de esquecer, automatizada.

plan.ts regex sweep err → para
4
A saída

injete o seam

Tempo e acaso são dependências. Clock (curator) e idFactory (clarify) são injetados: produção passa o real, o teste passa um fixo. Controle o seam, controle o replay.

5
Slide 1 / 5 use
13

Verifique


Revisão acumulada

Três perguntas. A pontuação corre conforme você responde — acerte as três e a revisão fecha.

1. Por que o id de um run deriva de um hash do seu spec, e não de um timestamp ou UUID?
Correto: c. runIdFor(spec) endereça o run por conteúdo. Um id por timestamp tornaria "o mesmo run" impossível de reencontrar amanhã e mudaria a cada chamada; um hash de conteúdo faz re-runs convergirem para o mesmo diretório e faz de um spec alterado um run distinto.
2. O scanner de determinismo rejeita Date.now(), new Date() e Math.random() num alembic.plan.ts. A razão central é:
Correto: b. Um plano é a descrição determinística de um run. Valores de relógio ou de acaso dariam estruturas diferentes a cada reavaliação, quebrando o replay por conteúdo. O scanner aplica o que um humano esqueceria — "remova-os do módulo de plano" é o erro nomeado.
3. O curator genuinamente precisa do "agora" para decidir obsolescência. Como ele obtém o tempo sem quebrar o determinismo?
Correto: d. Tempo é um efeito colateral, então é injetado como qualquer outro. O curator guarda limiares em ms e lê o "agora" do Clock — nunca de um global — então a mesma telemetria + o mesmo relógio sempre dão a mesma decisão de ciclo de vida (lição 9).
Acertos: 0/3
Você é o aluno e também o auditor: abra qualquer alembic.plan.ts e pergunte — "há um Date.now(), new Date() ou Math.random() escondido aqui?". Se houver, o run nem começaria. E pergunte de onde o tempo legítimo entra: deve ser sempre um Clock injetado, nunca um global. A seguir (lição 29): como estender a fusão — adicionar um novo subsistema sem quebrar nenhuma das quatro invariantes.