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".
runIdFor(spec) (a identidade do run) de freshId() (um id de correlação, descartável).events.jsonl + checkpoint.json + meta.json.Clock 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.
A identidade de um run É o hash do seu spec — por isso amanhã você ainda encontra exatamente este run.
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.
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?
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.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.
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
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
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.
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.
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();
freshId existe e é legítimo — para correlação. Para a identidade de um run, ele seria veneno: amanhã o run estaria perdido.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).
| Arquivo | O que guarda | Papel no replay |
|---|---|---|
events.jsonl | append-only; todo evento, em ordem | a história completa para reler |
checkpoint.json | o último estado retomável | de onde o resume continua |
meta.json | a impressão digital de goal/plan/contract | validada no --resume |
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.
/** 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, …
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().
// 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
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 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.
- **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.
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.
Cole mentalmente um plano no scanner abaixo e veja o veredito. Alterne entre um plano limpo e um plano com uma chamada proibida:
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.
// 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}`);
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.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.
O não-determinismo é uma dependência: bani-lo no plano, injetá-lo em todo o resto. Uma ideia, dois chapéus.
Clock do curatorO 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).
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.
// 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;
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).
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.
// default = um contador monotônico; nunca um global aleatório this.mintId = options.idFactory ?? monotonicIdFactory(); // … export const monotonicIdFactory = (prefix = 'clarify'): (() => ClarifyId) => { /* contador */ };
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.
Date.now()?Clock injetado (epoch ms). Produção passa o real; o teste passa um fixo. O global Date.now() nunca é tocado.freshId() (UUID aleatório) é descartável, só para correlação. runIdFor(spec) (hash do conteúdo) é a identidade do run.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.)
runIdFor garante.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.
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.
const id = Date.now() para o identificador e const prazo = new Date() para o "agora". Parece inofensivo./\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.freshId() no worker); se for identidade, deriva do conteúdo da unidade.Clock injetado (como no curator). Produção passa o relógio real; o teste passa um fixo — e a decisão fica reproduzível.const seed = Math.random(); return planFor(seed);O que acontece com const seed = Math.random() dentro do plano, e qual é o conserto determinístico?
/\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.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.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() é 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.Três perguntas. A pontuação corre conforme você responde — acerte as três e a revisão fecha.
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.Date.now(), new Date() e Math.random() num alembic.plan.ts. A razão central é: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).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.