Você já destilou o corpus, contratou a Iris e rodou a campanha da C.D. Agora veja o lado de construção: a Factory que põe agentes de código em caixas isoladas, o Forge de 7 passos que vira um pedido vago em escopo executável, e o course + @alembic/design — o mesmo motor que renderizou esta página que você está lendo.
renderHtmlPage, a função que monta o shell deste curso
Esta lição é um espelho: ela explica a maquinaria que a produziu. alembic course chama exatamente esta função para escrever o index.html e cada lição. Ler 100 linhas de render.ts = entender por que toda página do curso é byte-estável e passa o gate impeccable.
alembic alguma vez. Nada além disso.@alembic/factory (o sandcastle internalizado) faz e escolher entre seus 5 templates.alembic course: corpus → selectLocalLlmPackages → manifesto → renderHtmlPage.impeccable.Até agora o curso mostrou o Alembic produzindo: destilando conhecimento, orquestrando agentes, rodando campanhas. Esta lição vira a câmera para o outro lado — as três oficinas de construção que o operador usa para criar coisas novas.
São três máquinas distintas, cada uma resolvendo um problema diferente de "como construir":
@alembic/factory) — põe agentes de código (Claude, Codex, Copilot…) para trabalhar dentro de caixas isoladas (git worktrees + containers), seguindo um template de orquestração. É o chão de fábrica onde código é escrito sem bagunçar seu repositório.forge/plan) — pega um pedido vago ("quero uma fábrica de petições") e o destila, em 7 passos, num escopo executável: GOAL.md, um contrato de validação e um plano alembic.plan.ts. É o porteiro que transforma desejo em especificação.course (@alembic/coda + @alembic/design) — pega um corpus e gera um curso visual-teach byte-estável. É o motor que renderizou esta página.Pense como… uma marcenaria com três estações. A Factory é a bancada com tornos e morsas, onde a peça é cortada longe da sua mesa de jantar. O Forge é a prancheta onde o pedido do cliente vira um desenho técnico cotado. O course é a gráfica que imprime o manual do produto, sempre no mesmo papel timbrado. Onde a analogia quebra: a marcenaria precisa de um marceneiro; aqui as três estações são código puro que um agente opera sozinho — e duas delas rodam $0, offline, determinísticas.
As três estações vivem em pacotes diferentes do monorepo e têm naturezas opostas de execução. @alembic/factory é imperativo e com efeitos colaterais reais (cria worktrees, sobe containers Docker/Podman, invoca CLIs de agentes) — é o único dos três que precisa de IO pesado. Já @alembic/coda+@alembic/design são puros e determinísticos: sem Date.now(), sem Math.random(), sem rede — o curso é função pura do corpus. O forge fica no meio: cada um dos 7 passos é plugável e roda online (via LLM) ou offline (fallback determinístico), conforme a flag --online.
O fio que costura tudo: os três são acionados por um comando alembic <cmd> que segue o mesmo padrão (schema Zod em args.ts → run<Command> em commands.ts retornando Result<T, Error> → dispatch em index.ts). Nenhum deles lança exceção: falham fechado, devolvendo o erro como valor.
Antes do diagrama, fixe a distinção — é o erro nº 1 de quem começa: achar que "factory", "forge" e "course" são sinônimos. Eles operam em momentos diferentes da história da nossa fábrica de petições.
| Máquina | Entrada | Saída | Momento na história C.D |
|---|---|---|---|
Forge forge/plan | um pedido em texto | GOAL.md + contrato + alembic.plan.ts | "quero uma fábrica de petições" → escopo |
Factory @alembic/factory | issues + um template | código em branches isoladas | os agentes implementam a venture, em caixas |
course course | um corpus | curso HTML byte-estável | o manual de operação da Iris para a C.D |
REVIEW.md de observabilidade.renderHtmlPage de @alembic/design. runCourse a chama uma vez para o index.html e uma vez por lição — a mesma função que montou esta página.Date.now()/Math.random() lidos na saída. Um clock é injetado mas deliberadamente não usado no artefato. Mesmo corpus → bytes idênticos.As três oficinas no fluxo da nossa fábrica de petições, da esquerda (desejo) para a direita (produto entregue):
Result.course sozinho sobre qualquer corpus, ou a Factory sobre qualquer conjunto de issues, sem ter passado pelo Forge. Eles compõem, mas não dependem um do outro.Os cinco pontos que sustentam o resto da lição. Use as setas ← →.
@alembic/factory é uma fábrica de software: ela pega issues (tarefas) e um template de orquestração, e põe agentes de código — Claude Code, Codex, Copilot, Cursor, OpenCode — para resolvê-las dentro de ambientes isolados. "Isolado" aqui é literal: cada agente recebe um git worktree próprio (uma cópia da branch) dentro de um container (Docker ou Podman). Eles trabalham em paralelo sem pisar no seu repositório nem um no outro.
Esse pacote não foi escrito do zero. Ele é o sandcastle — uma fábrica open-source — internalizado (vendorizado) e adaptado às convenções do Alembic.
A própria descrição do pacote declara a origem: "Alembic software factory … Vendored and adapted from @ai-hero/sandcastle (MIT, (c) Matt Pocock); see README.md." Internalizar com crédito é a regra — nunca apagar a procedência.
A Factory não amarra você a um único agente. Ela fala com vários CLIs de código por trás de uma fachada comum:
AgentProvider.ts — você troca o agente sem mudar a orquestração.Os 5 templates que vêm na caixa — cada um é uma estratégia de orquestração diferente. Repare que o nome descreve o grafo de agentes:
"review" no nome adiciona o passo de revisão; "parallel" abre branches simultâneas. (Grafos: interpretação da descrição de cada template; o JSON traz só nome+descrição.)O parallel-planner precisa instalar a dependência zod no host, mas o simple-loop não. Por quê só o planner precisa de Zod?
Porque o template planner faz o agente emitir um plano estruturado (uma saída <plan>) que precisa ser validado contra um schema. O TemplateMetadata declara isso em dependencies: ["zod"], e o init oferece instalar — senão npx tsx .sandcastle/main.ts quebraria com ERR_MODULE_NOT_FOUND. O simple-loop só "pega issue, fecha issue": não tem saída estruturada, então não precisa de validador.
git push.O Forge resolve o problema mais humano de todos: você quer algo ("uma fábrica de petições personalizadas para a C.D"), mas um pedido em uma frase não é executável. O Forge é um funil de 7 passos que coa esse desejo até virar um escopo que o harness consegue rodar sozinho.
Cada passo produz um artefato concreto, escrito num diretório de escopo. Você nomeia o pedido e o Forge materializa a pasta inteira:
Veja o passo a passo de perto — clique em cada um:
grill
→ SCOPE.md
Interroga o pedido. "Fábrica de petições" para quem? quais tipos de petição? o que é sucesso? Sai um SCOPE.md com o problema afiado.
packages/forge/src/scope.ts) copia o GOAL.md, o plano e o contrato para o diretório da run — é o primeiro dos 5 gates do pipeline (Scope → Council → Proof → Validator → Publish). Assim a run executa contra uma cópia congelada do escopo, não contra arquivos que podem mudar embaixo dela.--online. O default é offline, $0 — você forja o esqueleto de graça e só paga quando quiser a versão pensada por um modelo.O front-end de 7 passos completo. Materializa a pasta de escopo inteira — SCOPE/RESEARCH/PROTOTYPE/PRD/issues/GOAL/contrato/plan/REVIEW.
O atalho: gera direto o plano HTML + GOAL.md + contrato + alembic.plan.ts a partir do pedido, sem os 7 passos. É o @alembic/planf3 por baixo (o Forge também o usa internamente).
Agora o espelho. O comando alembic course pega um corpus e gera um curso visual-teach — um index.html + uma página por lição. E ele faz isso com a mesma máquina que produziu a página que você está lendo: @alembic/design, o design system Warm-Neutral como dados tipados + renderizadores puros.
A cadeia tem três elos, e cada um vive num pacote:
coda escolhe o conteúdo; o contracts fixa o formato; o design pinta o HTML — sempre nas mesmas cores.O runCourse recebe um clock injetado (para honrar a assinatura padrão), mas deliberadamente não o lê na saída: o manifesto não carrega timestamp. Resultado: rode o mesmo corpus dez vezes, e os dez cursos são byte por byte idênticos.
Isso não é preciosismo. É o que torna a marca governável: como o HTML é determinístico, o gate impeccable consegue detectar qualquer "drift" — uma cor fora do sistema, um heading pulado — porque sabe exatamente quais bytes esperar. Os tokens de cor vivem como dados validados (THEME_REGISTRY), copiados verbatim de docs/design-system.html.
O docstring é a lei: "Every token value below is copied VERBATIM from the live :root block of docs/design-system.html … these values are what the impeccable CI detector keys off, so a drift here is a brand-governance bug."
impeccable consegue auditá-lo: há uma única fonte da verdade.Os mesmos nomes de token (clay, olive, ivory…) ganham valores diferentes sob :root[data-theme="dark"]. renderHtmlPage emite o bloco escuro só quando darkMode está ligado e o preset tem paletteDark — senão a página é puro modo claro (o artefato que o gate impeccable valida sem ruído). É por isso que o botão ☾/☀ no topo desta página funciona.
Sutileza que vale ouro: esta página foi escrita à mão a partir do shell visual-teach; o alembic course gera as páginas dele com @alembic/design. São duas fontes diferentes do mesmo design system. Compare o token olive:
| Token | Shell desta página (lesson-template) | @alembic/design (themes.ts) | Igual? |
|---|---|---|---|
| olive | #788C5D | #788C5D | ✓ idêntico |
| oat | #E3DACC | #E3DACC | ✓ idêntico |
| ivory | #FAF9F5 | #FAF9F5 | ✓ idêntico |
| clay | #D97757 | #E75533 | ≈ mesma família |
clay tem hexes ligeiramente diferentes entre as duas fontes (o shell calibrou o tom para os dois temas; o themes.ts traz o valor de marca de docs/design-system.html). É a prova viva de por que o gate impeccable existe — para que essas pequenas derivas sejam intencionais e rastreáveis, não acidentes. Os SVGs desta lição usam #D97757 (a paleta do shell) por consistência com a página.packages/coda/src/publish.ts) o publica: gist privado + Cloudflare Pages. É o mesmo padrão que levou o curso anterior do produto ao ar. Gerar ≠ publicar: a publicação é uma ação outward, sempre explícita.O elo central, sem retoque: dentro de runCourse, a mesma renderHtmlPage é chamada para o índice e para cada lição. Esta é a linha que torna a lição um espelho.
// puro e $0: o manifesto + HTML são função pura das entradas // (sem clock/RNG lido na saída) → mesmo corpus = curso byte-idêntico. const indexHtml = renderHtmlPage({ preset, title: manifest.title, body: renderCourseIndexBody(manifest), lang: 'en', nav: { lessons: courseIndexNav, lessonsLabel: 'Course' }, }); // …e uma vez por lição: for (const lesson of manifest.lessons) { const lessonHtml = renderHtmlPage({ preset, title: lesson.title, body: renderCourseLessonBody(lesson), nav: { /* on-this-page + índice do curso */ }, }); }
E a assinatura do renderizador — note que ele devolve uma string de HTML self-contained, e que darkMode e nav são opcionais (o curso byte-estável e esta página usam ambos):
export interface RenderHtmlPageInput { readonly preset: ThemePreset; // os tokens Warm-Neutral readonly title: string; readonly body: string; // HTML cru, inserido após o <h1> readonly lang?: string; // PT-BR é o default readonly darkMode?: boolean; // toggle embutido; default true readonly nav?: RenderHtmlPageNav; // sidebar opcional }
As três máquinas chegam ao usuário pelo mesmo wiring de 3 arquivos — o padrão de qualquer comando alembic:
forge, course e os comandos da Factory — previsível de ler e de testar.E o lugar do Forge no quadro maior: ele alimenta os 5 gates do pipeline. O Scope Gate (de forge/src/scope.ts) é o primeiro; o Publish Gate (de coda/src/publish.ts) é o último — o mesmo que publica o curso.
Clone o repo e abra os três arquivos lado a lado: apps/cli/src/commands.ts (busque runCourse), packages/design/src/render.ts (a função renderHtmlPage) e packages/factory/src/InitService.ts (a constante TEMPLATES e listTemplates). Rode pnpm --filter @alembic/design test para ver os testes de byte-estabilidade e de contraste do tema passarem.
O @alembic/design exporta: WARM_NEUTRAL, DEFAULT_THEME_ID, THEME_REGISTRY, pickTheme, e os renderizadores renderThemeCss / renderStyleTag / renderHtmlPage. Tudo puro, depende só de @alembic/contracts.
Hora de pôr a mão. Primeiro um exemplo resolvido (como o operador forja o escopo da fábrica de petições), depois um seletor de templates ao vivo.
alembic forge "fábrica de petições personalizadas para a C.D Advocacia" — o Forge cria a pasta de escopo a partir do slug do pedido.issues.jsonl (as tarefas); o passo 6 escreve GOAL.md, o validation-contract.md e o alembic.plan.ts — o plano que o harness executa.REVIEW.md (observabilidade). O Scope Gate copia GOAL+plano+contrato para a run, e a venture está pronta para alembic run --goal GOAL.md --plan alembic.plan.ts --yes.--online primeiro, e por quê? (Dica: onde o pensamento de um modelo agrega mais — o research ou o issues?)Clique num template. O grafo de agentes e a "configuração derivada" mudam — exatamente como a Factory deriva os passos de setup a partir do nome do template (name.includes("review") adiciona o revisor; dependencies com zod pede o validador de schema).
TEMPLATES em packages/factory/src/InitService.ts. Os grafos são a interpretação visual de cada descrição.As três máquinas dividem o trabalho de "construir": o Forge decide o que fazer (escopo), a Factory faz (agentes em caixas) e o course documenta (o manual). Duas delas são de graça e offline; só a Factory mexe no mundo de verdade.
Naturezas de execução opostas: @alembic/factory é imperativo com IO real (worktrees, containers, CLIs de agente — construído sobre Effect); @alembic/coda+@alembic/design são puros/determinísticos (sem clock/RNG/IO, dependem só de @alembic/contracts); o forge é híbrido com passos plugáveis online/offline. Todos seguem o contrato Result<T, Error> e o wiring args.ts → commands.ts → index.ts.
Revisão — fixe os três
@alembic/factory internaliza, e qual é a regra ao fazê-lo?package.json declara: "Vendored and adapted from @ai-hero/sandcastle (MIT, (c) Matt Pocock)". Internalizar com procedência é a regra de ouro — nunca apagar o crédito.runForgeFrontEndCli em commands.ts: "grill → research → prototype → PRD → issues → GOAL → review". (A opção b são as fases de uma run, não os passos do Forge.)alembic course é byte-estável?runCourse é explícito: "o manifesto + HTML são função pura das entradas (sem clock/RNG lido na saída)". Determinismo na origem — não um passo de normalização posterior — é o que o impeccable consegue auditar.Acertos: 0/3
As Dez sobre Factory, Forge & cursos
alembic.plan.ts executável.@alembic/planf3); forge é o funil completo.selectLocalLlmPackages → courseManifest → renderHtmlPage.@alembic/design renderizou esta página — o curso é um espelho.impeccable detecta drift de marca.alembic course <corpus> sobre qualquer pasta de records e abra o index.html gerado — compare o shell dele com esta página. Que token você mudaria primeiro em themes.ts, e o que o gate impeccable diria? Próxima lição (0008): amarramos tudo — corpus → signal → venture → C.D → Iris — em três walkthroughs ponta a ponta.