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.

Insight central: a camada Engine nunca importa da camada Products. As Capabilities ficam no meio, neutras, e podem ser usadas tanto pelo motor (ex.: @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):

Products on the engine marketing-factory factory forge planf3 design docs tui web infra app/cli Capabilities (offline $0 default) embeddings ocr vision prompts sessions automation hermes loop-engineering Engine (the motor) @alembic/contracts narrow waist etl ingestion adapters council swarm mission vm harness coda gates: Scope · Council · Proof · Validator · Course · Publish (em @alembic/coda) tiers: T1 cheap · T2 balanced · T3 strongest · T4 human · LOCAL offline (em @alembic/contracts/tier)

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:

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.