Curso / Lição 28  ·  EN
Lição 28 · Avançado · aprofunda o invariante 3

Determinismo & replay: a mesma entrada, a mesma run

O invariante 3 (Lição 16) diz que uma run é content-addressed e replayável. Esta lição é o mecanismo: o id de uma run é um hash do seu spec, então o mesmo spec sempre cai no mesmo diretório; esse diretório é um log de eventos append-only mais um checkpoint, então uma run que crashou retoma; e a VM de plano proíbe as três funções que quebrariam tudo isso — Date.now(), new Date() e Math.random(). Onde tempo real e aleatoriedade real são genuinamente necessários, eles entram por uma costura injetada — um Clock e uma fábrica de ids — nunca um global. Essa única disciplina é o que torna "fazer replay desta run exata" possível.

IDs de run content-addressed

A identidade de uma run não é um timestamp nem um UUID — é o hash de conteúdo do seu spec. runIdFor(spec) faz o hash da especificação da run, então mudar qualquer campo do spec gera um novo id e, portanto, um novo diretório:

// packages/swarm/src/orchestrator.ts:168 — o id da run é o hash do seu spec
const runId = runIdFor(spec);   // hash de conteúdo estilo SHA-256 do spec
// muda qualquer campo de `spec` ⇒ um runId diferente ⇒ um diretório de run diferente
// (packages/swarm/src/types.ts:247-257)

O retorno: identidade é conteúdo. Duas runs do mesmo goal+plan+contract compartilham um diretório e podem retomar uma à outra; um spec ajustado é inequivocamente uma run diferente. Não há id derivado do clock que tornaria "a mesma run" não-encontrável amanhã.

O diretório de run determinístico

Cada run vive num caminho fixo e previsível com um log append-only e um checkpoint — o substrato para retomar e replay (Lição 16):

// packages/etl/src/run-directory.ts:52-59 — o layout
<baseDir>/runs/<runId>/
  ├── events.jsonl     // append-only; todo evento em ordem
  ├── checkpoint.json  // último estado retomável
  └── meta.json        // fingerprint de goal/plan/contract (validado no --resume)

E os stores content-addressed reforçam: resultados são gravados por SHA-256 sobre JSON canônico (chaves ordenadas), então "re-anexar conteúdo idêntico é um no-op e re-runs convergem" (etl/stores.ts:79-97). A idempotência é estrutural — rodar duas vezes não pode duplicar, e uma run retomada pega exatamente onde o log parou.

specgoal+plan+contract runIdFor(spec)hash SHA-256 runs/<runId>/ events.jsonl (append-only) checkpoint.json replay / resumerelê o log

A proibição: sem Date.now(), new Date(), Math.random() num 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 de parede ou jogar dados, duas avaliações do mesmo plano divergiriam — e o replay seria uma mentira. Então a VM de plano rejeita as três na avaliação (complete-map §7.8; CLAUDE.md "Determinism"):

// alembic.plan.ts — estas lançam na avaliação da VM de plano:
const id = Date.now();        // ✗ rejeitado — relógio de parede
const t  = new Date();         // ✗ rejeitado — relógio de parede
const r  = Math.random();      // ✗ rejeitado — não-determinismo

O erro é nomeado no guia de troubleshooting: "Non-determinism error — remova Date.now(), new Date(), Math.random() do módulo de plano". A VM não confia que você lembre; ela impõe. (Nota: essa proibição é 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 descrevendo uma run não pode.)

A saída de emergência: injete o clock e a fábrica de ids

Sistemas reais precisam de tempo real (um curador decidindo "obsoleto após 30 dias") e ids únicos (uma pergunta clarify precisa de um handle). A resposta não é proibi-los — é torná-los uma costura injetada, para que a produção passe o real e um teste passe um fake. Você já viu ambos:

O princípio: não-determinismo é uma dependência, então injete-o

Tempo e aleatoriedade são efeitos colaterais, exatamente como o filesystem. O segundo invariante do motor — kernel puro, efeitos colaterais injetados — se aplica a eles também. Proibir os globais na VM de plano e passar um Clock/fábrica de ids em todo o resto é uma ideia usando dois chapéus: o único não-determinismo numa run entra por uma costura que você controla. Controle a costura e você controla o replay. Os subsistemas curator e clarify guardam thresholds em milissegundos precisamente para que o Clock injetado seja a única fonte de tempo.

Por que isso é a rocha do replay

Junte tudo. IDs content-addressed significam que "a mesma run" é encontrável. O log append-only + checkpoint significam que uma run pode ser relida e retomada. A proibição da VM de plano significa que re-avaliar o plano gera a estrutura idêntica. O Clock/fábrica de ids injetados significam que o não-determinismo residual é capturado e replayável. Remova qualquer um e o replay quebra: um id derivado do clock não-encontraria a run; um ramo aleatório no plano re-rodaria diferente; um Date.now() global no curador faria "obsoleto" depender de quando você fez o replay. A disciplina é holística — que é por que a VM impõe a parte fácil de esquecer automaticamente.

1. Por que o id de uma run é derivado de um hash do seu spec em vez de um timestamp ou UUID?
Correto: c. runIdFor(spec) content-addressa a run. Um id de timestamp tornaria "a mesma run" não-encontrável amanhã e mudaria a cada invocação; um hash de conteúdo faz re-runs convergirem ao mesmo diretório e faz um spec ajustado uma run distinta.
2. A VM de plano 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 uma run. Valores de relógio de parede ou aleatórios gerariam estruturas diferentes na re-avaliação, quebrando o replay content-addressed. A VM impõe o que um humano esqueceria — "remova-os do módulo de plano" é o erro nomeado.
3. O curador genuinamente precisa do "agora" para decidir obsolescência. Como ele obtém tempo sem quebrar o determinismo?
Correto: d. Tempo é um efeito colateral, então é injetado como qualquer outro. O curador guarda thresholds em milissegundos e lê o "agora" da porta Clock — nunca um global — então a mesma telemetria + o mesmo clock sempre geram a mesma decisão de ciclo (Lição 9).

Confusões comuns

"A proibição significa que o Alembic nunca pode usar o tempo atual." Ela proíbe os globais no módulo de plano, onde o não-determinismo quebraria o replay. Em todo o resto, tempo entra por um Clock injetado — totalmente usável, só controlável. Um comando de CLI pode ler o clock; um plano descrevendo uma run não pode.
"IDs content-addressed são só para dedup." Dedup é um benefício (re-anexar conteúdo idêntico é um no-op), mas o propósito mais profundo é replay: o id é o fingerprint do spec, então resume/replay pode encontrar e re-rodar exatamente a mesma run. Identidade e idempotência caem do mesmo hash.