Curso · produto
Alembic — Visão geral & arquitetura
O que é o Alembic
O Alembic é um motor de destilação acoplado a um harness de agentes. Ele lê um corpus de conhecimento bruto (transcripts, bookmarks, repos, notas) e o transforma em duas saídas operacionais: LEARNINGS (o que aprendemos, citável) e BUSINESS SIGNALS (o que poderia virar produto). Esses sinais alimentam uma holding multi-venture — o motor é interno, as ventures que rodam sobre ele é que são o produto para fora (ver docs/adr/0001-alembic-internal-engine-not-product.md).
Esta primeira lição cobre o terreno: as três camadas, as convenções de engenharia que se repetem em todo pacote, e o mapa dos 27 pacotes que compõem o motor hoje (1364 testes verdes, 18 ADRs registradas, 98 PRs mergeados). As lições seguintes abrem cada peça em detalhe; aqui você ganha o esqueleto para navegá-las.
As três camadas
O Alembic é desenhado em três camadas concêntricas. Cada uma só conhece o que está abaixo:
1. Engine (o motor)
Os blocos de infraestrutura que não mudam com o caso de uso: contracts (schemas + Result<T, E>), etl + ingestion (corpus, PII, FsPort), adapters (registry de modelos + tier routing), council (debate multi-perspectiva + context-pack), swarm + mission + vm (orquestração e plan determinístico), harness (transporte HTTP/SSE/MCP), coda (gates: Scope, Proof, Validator, Course, Publish).
2. Capabilities (os tijolos $0)
Funcionalidades offline-default que qualquer agente ou pipeline pode chamar: embeddings, ocr, vision, prompts (defesa contra injection), sessions (leitor de sessões codex), automation (manifests TOML de cron), hermes (memory multi-store + soul + AI-Employee), loop-engineering (LEARN→ANALYZE→EXECUTE→VERIFY→DECIDE).
3. Products-on-the-engine (as ventures)
O que sai para fora: marketing-factory (campanhas multi-tenant via Higgsfield), factory (o sandcastle vendor — software factory), forge + planf3 (front-end de escopo em 7 passos), design (o tema Warm-Neutral e o renderHtmlPage deste curso), docs, tui, web, infra. E o app/cli (o binário alembic em si) que costura tudo.
@alembic/embeddings dentro do distill) quanto por uma venture (ex.: o mesmo @alembic/vision validando frames de vídeo no marketing validate). É essa direção de dependência que mantém o motor reaproveitável.
As convenções que se repetem em todo pacote
Cinco regras valem para o monorepo inteiro — entendê-las uma vez é entender qualquer arquivo:
Monorepo pnpm + ESM + TypeScript strict
Workspaces em packages/* e apps/*. Resolução NodeNext, sem CommonJS. Build/teste rodam com Turborepo:
pnpm -r typecheck && pnpm -r build && pnpm -w test
@alembic/contracts é o "narrow waist"
Todo schema compartilhado vive em packages/contracts/src/ — domain.ts, model.ts, registry.ts, tier.ts, theme.ts, wiki.ts, session.ts, course.ts, otel.ts — e exportado por index.ts. Qualquer pacote que precisa falar com outro passa por aqui. Mudar um schema sem atualizar testes + caminho de compatibilidade é proibido (ADR-0008, ADR-0009).
Result<T, Error> em vez de throw
Biblioteca não lança exceção. Toda operação falível devolve um Result com discriminante ok: true | false (ADR-0009: narrow waist — run never throws):
export type Result<T, E = Error> = Ok<T> | Err<E>;
export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });
Falhas viram valores — propagam pelo grafo sem desviar o controle de fluxo. O Proof Gate trata um err como reprovação fechada (fail-closed).
FsPort em vez de fs direto
Em vez de chamar node:fs espalhado, pacotes recebem um FsPort via injeção. Em teste, um fake; em produção, o real (de @alembic/etl). Resultado: cada pacote é testável sem tocar o disco — e a mesma função roda offline ou contra um sandbox.
Determinismo + offline-default, --online gated
Plan modules (alembic.plan.ts) não podem chamar Date.now(), new Date() ou Math.random() — a VM rejeita. Comandos rodam offline / $0 por padrão (adapter determinístico LOCAL). Para tocar um modelo real ou sair na rede, o usuário precisa passar --online (e, no caso de gasto pesado, também --approve --yes). Spend-safe por construção.
Validação de saúde do motor
Para confirmar que o motor está coeso, rode o doctor:
$ alembic doctor --client-stack
[OK] model-registry: 11 model(s) validated
summary: 2 ok, 0 warn, 0 fail
O comando doctor --client-stack valida o MODEL_REGISTRY (packages/contracts/src/registry.ts) e a coerência dos adapters — tudo offline, sem rede, $0. Passando --online, ele faz preflight do gateway cliproxyapi em 127.0.0.1:8317 e roda um smoke de completion por modelo (fail-closed).
Mapa dos 27 pacotes
O diagrama abaixo agrupa os 27 pacotes pela camada a que pertencem. A direção das setas é a direção das dependências permitidas (de cima para baixo):
Os 27 nomes do diagrama batem exatamente com ls packages/ no repo. O app/cli (em apps/cli/) é o único app que vive fora de packages/; ele é o binário alembic que você invoca na linha de comando.
Como funciona por dentro — por que o motor não vaza para os products
Cada pacote tem um package.json que declara explicitamente suas dependencies. Se você abrir, por exemplo, packages/etl/package.json, vê @alembic/contracts mas nunca @alembic/marketing-factory. A direção é estrutural, não convencional: o pnpm -r build falharia se uma camada baixa tentasse importar uma alta, porque o grafo do workspace é resolvido em ordem topológica.
O @alembic/contracts não tem dependência de runtime alguma — é zod + tipos puros. Por isso ele pode ser importado por qualquer outro pacote sem criar ciclo. É o narrow waist no sentido literal: tudo passa por ele, ele não passa por nada.
Os tiers (T1/T2/T3/T4/LOCAL, em packages/contracts/src/tier.ts) são a única escada de escalação (ADR-0004). T4 é a porta humana — runs param ali e esperam alembic approve --task-id <id> ou alembic reject --task-id <id>. É como o motor mantém ações irreversíveis sob controle humano, sem precisar de uma camada de governance separada.
Onde ir agora
As próximas lições destrincham cada bloco do mapa:
- funnel —
distill/ingest/wiki-bridge/embed-index/vision-index/status: como o corpus vira LEARNINGS + SIGNALS via os tiers T0→T3. - capabilities —
embed/ocr/triage/notes/context-pack/memory: os tijolos offline $0. - swarm & harness —
run/tui/tail/serve/cockpit+ missões + pipeline de gates. - AI Employee —
employee show/explain/run/connectors/schedule: o maestro que compõe soul + skills + memória + connectors + schedule. - marketing factory —
marketing discover/campaign/video/validate: a fábrica multi-tenant sobre o Higgsfield. - factory / forge / course —
@alembic/factory(sandcastle),forge/plan,course+@alembic/design(o tema deste próprio curso). - casos de uso — três walkthroughs ponta-a-ponta amarrando todos os blocos.
Auto-verificação
O que separa as 3 camadas — e qual é a regra de dependência entre elas?
Engine (infraestrutura do motor: contracts, etl, adapters, council, swarm, harness, vm, coda, mission, ingestion), Capabilities (tijolos offline $0: embeddings, ocr, vision, prompts, sessions, automation, hermes, loop-engineering) e Products on the engine (as ventures: marketing-factory, factory, forge, planf3, design, docs, tui, web, infra, e o app/cli). A regra é uma direção só: products podem importar capabilities, capabilities podem importar engine, mas nunca o contrário. O grafo do pnpm workspace força isso topologicamente.
Por que Result<T, Error> em vez de throw nas libs?
Porque falhas viram valores de primeira classe (ADR-0009, narrow waist — run never throws). Um err propaga pelo grafo sem desviar o controle de fluxo; o Proof Gate trata como reprovação fechada (fail-closed). Sem exceções escapando, o motor é determinístico de ponta a ponta — o que torna o teste, o resume e o cache (chave SHA-256 de (prompt, opts)) confiáveis.
Por que offline-default + --online gated, e quem decide ir online?
Para que rodar o motor seja sempre spend-safe por construção: zero rede, $0, determinístico, em qualquer máquina. --online opta por um backend real (cliproxyapi, brightdata, MLX-VLM, higgsfield). No caso de gasto pesado — geração de vídeo paga, por exemplo — é preciso dois flags simultâneos: --approve --yes. Um único flag nunca gasta. A decisão é do humano que invoca o CLI; o motor nunca opta sozinho.