Você leu sete subsistemas entregues e construiu dois próprios nos labs. Agora destile isso num checklist que um agente futuro pode seguir para adicionar um oitavo subsistema @alembic/hermes sem redescobrir as convenções. A receita não é gosto arbitrário — cada passo remete a um invariante ou ADR que você já conheceu. Siga-a e seu subsistema vai parecer que sempre esteve lá; pule um passo e o CI ou um revisor te pega, porque as regras são impostas, não sugeridas.
packages/hermes/src/<name>/ com três arquivos: types.ts (schemas Zod + interfaces de porta), a implementação (uma classe ou funções sobre portas injetadas) e <name>.test.ts (fakes para cada porta). Todos os exports são nomeados e re-exportados de src/index.ts. É o template inteiro — memory, learning, clarify, web, skills, curator e media todos seguem.| Passo | Faça | Porque (invariante / ADR) |
|---|---|---|
| 1 | Defina portas — passe IO/tempo/aleatoriedade como interfaces injetadas (FsPort, um backend, Clock, uma fábrica de ids) | Invariante 2: kernel puro, efeitos colaterais injetados (ADR-0009) |
| 2 | Valide toda entrada com Zod safeParse na fronteira | Entradas podem ser saída não confiável de modelo/rede (ADR-0011) |
| 3 | Retorne Result<T, Error> de toda função falível; nunca lance na fronteira pública | A cintura estreita / nunca-lança (ADR-0009) |
| 4 | Sem Date.now()/new Date()/Math.random() — injete um Clock/fábrica de ids | Determinismo & replay, invariante 3 |
| 5 | Não adicione nova dependência de runtime sem justificativa | "Rules for safe changes" (CLAUDE.md) |
| 6 | Adicione o vitest.config.ts por pacote endurecido + rode via test:safe | Segurança de testes anti-órfão (Lição 25) |
| 7 | Exporte símbolos nomeados de src/index.ts; documente a proveniência CLONE/ADAPT + fontes | Descobribilidade + clean-room (ADR-0011 §4) |
| 8 | Mantenha a suíte verde; a contagem total de testes deve aumentar ou ficar estável | Toda feature precisa de testes (CLAUDE.md) |
Comece com types.ts. Defina o que uma entrada válida é (Zod) e do que o subsistema depende (interfaces de porta). Os subsistemas entregues fazem exatamente isso — ex.: as portas WebBackend + Compressor do subsistema web, as portas ReviewProposer + ReviewGate do loop de aprendizado. Portas são tipos, nunca classes concretas:
// packages/hermes/src/<name>/types.ts import { z } from 'zod'; export const requestSchema = z.object({ /* … campos limitados … */ }); export type Request = z.infer<typeof requestSchema>; // Uma porta = uma capacidade injetada, nunca um import concreto: export type Backend = (req: Request) => Promise<import('@alembic/contracts').Result<Response, Error>>; // nunca lança
O arquivo de implementação é funções ou uma classe sobre essas portas. Todo caminho falível retorna Result; IO é envolvido em tryCatchAsync; tempo/aleatoriedade vêm de um Clock/fábrica de ids injetados, nunca um global (Lição 28). É o esqueleto exato do Lab 1 — não é um brinquedo, é o padrão de produção no tamanho mínimo.
// packages/hermes/src/<name>/<name>.ts import { ok, err, tryCatchAsync, type Result } from '@alembic/contracts'; import { requestSchema, type Backend } from './types.js'; export const doThing = async ( input: unknown, deps: { backend: Backend; now: () => number }, ): Promise<Result<Response, Error>> => { const parsed = requestSchema.safeParse(input); // ② Zod na fronteira if (!parsed.success) return err(new Error(parsed.error.message)); return deps.backend(parsed.data); // ③ Result passa, ④ sem globais };
Antes de adicionar um pacote, pergunte: um global já no runtime consegue fazer isso? Os subsistemas web e media responderam que sim — usam o fetch global via um fino createFetchBackend em vez de adicionar um cliente HTTP (Lição 11). O subsistema skills escreveu seu próprio parser de frontmatter escalar sem dependências em vez de puxar YAML (Lição 12). A regra: "Não adicione novas dependências sem justificativa." Uma nova dep é uma superfície de supply-chain e um risco de clean-room — mereça-a ou evite-a.
Cada pacote carrega seu próprio vitest.config.ts (o pacote hermes tem um). Combine a config root endurecida — timeouts limitados e pool:'forks' — e sempre rode a suíte pelo wrapper de grupo de processo, nunca vitest puro:
# rode a suíte inteira com segurança (Lição 25): pnpm test:safe # run limitado no próprio grupo → mata o grupo + varredura # ou um pacote: pnpm --filter @alembic/hermes test
testTimeout/teardownTimeout/forks (Lição 25) é parte do contrato — o wrapper de segurança de testes é o piso, a config é a primeira linha.Re-exporte todo símbolo público de src/index.ts com um comentário de cabeçalho nomeando a proveniência CLONE/ADAPT/IGNORE e as seções do mapa-fonte (leia o bloco de qualquer subsistema em index.ts — todos fazem isso). Depois rode a baseline e confirme que a contagem moveu na direção certa:
# a baseline de build/test que toda mudança deve manter verde (CLAUDE.md):
pnpm -r typecheck && pnpm -r build && pnpm -w test
A disciplina fecha aqui: "Toda feature nova precisa de testes; a contagem total de testes deve aumentar ou ficar estável." Um subsistema sem testes, ou um que derruba a contagem, não está pronto.
packages/hermes/src/<name>/ com types.ts + <name>.ts + <name>.test.tssafeParse na fronteiraResult<T, Error>; nunca lança publicamenteFsPort, backend, Clock, fábrica de ids) — sem node:fs, sem Date.now(), sem Math.random()test:safesrc/index.ts com proveniência + fontespnpm -r typecheck && pnpm -r build && pnpm -w test verde; contagem estável-ou-acimafetch global via um fino backend — sem dependência HTTP. E o backend é injetado, então testes passam um fake em vez de bater na rede. "Não adicione novas dependências sem justificativa" (CLAUDE.md).Clock torna o ciclo determinístico no teste e replay-safe em produção (Lição 28), e mantém o código uniforme com o resto do pacote.test:safe?test:safe (Lição 25) garante que um teste travado não fixe CPU por horas. A receita assa ambos na definição de "pronto", para que um agente futuro não possa pular nenhum em silêncio.node:fs economiza três linhas e te custa testes em memória, replay determinístico e um backend trocável. O padrão escala para baixo limpo.Date.now(), o CI roda os tipos em forma de nunca-lança, o wrapper de testes mata órfãos, e um revisor checa a proveniência. Desviar não é um debate de estilo — é um build falhando ou uma mudança rejeitada.