Passo 1 · Alembic Completo · Camada L0 · Contracts + ETL
Alembic Completo · Currículo fonte-de-verdade · Lição 1

Camada L0 · Contracts + ETL

A fundação de tudo. Os contratos definem a cintura estreita do sistema; o ETL T0 é o único tier que sempre roda, é 100% determinístico e custa $0. Tudo que chega caro já passou por aqui.

Cada seção tem um cartão Técnico opcional. Abra quando quiser o detalhe linha-a-linha.
Ao terminar esta lição, você consegue…
  • Explicar por que Result<T,E> e ModelRunResult formam a cintura estreita que toda a stack respeita.
  • Descrever as cinco etapas do pipeline T0: dedupe SHA-256 → validar contrato → score → rotear por prior → emitir.
  • Nomear os seis eixos do score (0–5 cada, 0–30 no total) e a evidência estrutural que alimenta cada um.
  • Dizer quais famílias viram residue para tiers caros e quais ficam baratas em T0/T1.
  • Explicar o portão assertRedactedForEmit e por que ele é fail-closed.
  • Rodar a camada localmente e inspecionar o residue, o ledger e os scores.
O que assumimos de você
  • Sabe ler TypeScript o suficiente para reconhecer um tipo e uma função.
  • Já ouviu falar de hash (SHA-256) como "uma impressão digital de um conteúdo". Se não — explicamos no caminho.
  • Nada mais. Os termos tier, gate, residue e narrow waist são definidos aqui.
01

A grande ideia

Barato e seguro, antes de qualquer coisa cara

O Alembic é um motor de destilação: ele ingere muito conteúdo bruto e deixa subir, para modelos caros, só o que vale a pena. A camada L0 é o chão dessa pirâmide. Ela tem duas peças que parecem distintas mas resolvem o mesmo problema — manter o sistema barato, testável e seguro:

@alembic/contracts — os tipos compartilhados (esquemas Zod). Definem a forma exata do que entra e sai de cada parte. É o vocabulário comum que deixa adapters, council e harness serem agnósticos de provider.
@alembic/etl — o pipeline T0: o único tier que sempre executa, sem humano no loop, sem chamar modelo, de graça. Ele filtra, valida, pontua e roteia o corpus bruto.
Guarde istoTier é o nível de autonomia do trabalho. T0 é "silencioso e totalmente autônomo, $0". A ladder vai de T0 (autônomo) até T4 (parked — exige council + humano). L0 é a camada de código que implementa o tier T0.
T0autônomo · $0 T1log leve T21 revisor T3council T4 · PARKcouncil + humano + autonomia + supervisão humana
packages/contracts/src/tier.ts — L0 implementa o piso (T0); o resto é o que o T0 deixa subir.
Analogia. Pense numa triagem de pronto-socorro. Antes de chamar o especialista caro, alguém na entrada mede sinais vitais, descarta duplicados, e decide quem pode esperar e quem sobe. O T0 é essa triagem: rápido, padronizado, gratuito — e ninguém caro é incomodado por um caso que a triagem já resolveu.

O contrato do tier

O enum Tier (em packages/contracts/src/tier.ts) define T0…T4 e um marcador separado LOCAL. As invariantes documentadas: T0 = silencioso/autônomo; T1 = autônomo com log leve; T2 = um revisor notificado; T3 = council obrigatório; T4 = PARK (council + humano). L0 implementa o comportamento de T0; os tiers acima são o trabalho que o T0 deixou subir.

02

O mapa do substrato

Em uma imagem: o corpus entra pela esquerda, passa pela cintura estreita e por quatro etapas determinísticas, e sai como scored (fica barato em T0) ou como residue (sobe para tiers caros). Repos de pesos e prompts são excluídos.

Mapa da camada L0: o corpus .jsonl entra à esquerda, passa pela cintura estreita Result + ModelRunResult e por quatro etapas (dedupe SHA-256, validar contrato, score de 6 eixos, rotear por prior), saindo como scored em T0 grátis ou como residue para os tiers caros; Repos/Models e Repos/Prompts são excluídos.

L0 de ponta a ponta — leitura da esquerda (corpus bruto) para a direita (tiers caros). A cintura estreita aperta tudo no meio.

corpus .jsonlBookmarks · Transcripts dedupeSHA-256 validarcontrato Zod score6 eixos · 0–30 rotearpor prior scored (T0)$0 residue→ T1+
packages/etl/src/pipeline.ts — read-only no source, append-only nos outputs.
03

@alembic/contracts — o spine

Os tipos que toda a stack respeita

Em packages/contracts/src/ moram os esquemas Zod-first e os tipos compartilhados. Qualquer mudança aqui é uma mudança de contrato sistêmica: ela atinge adapters, council, harness e factory de uma vez.

ArquivoO que define
result.tsResult<T,E> (ok/err) + tryCatch/tryCatchAsync — para operações falíveis que não chamam modelo.
model.tsModelRunInput + ModelRunResult — o verdadeiro narrow waist: toda chamada de modelo passa por aqui.
tier.tsEnum T0…T4 + marcador LOCAL + helpers de ordem.
registry.tsMODEL_REGISTRY + pickCheapestForTier (visto a fundo na lição 2).
wiki.ts · domain.tsWikiPackage, BusinessSignal, Learning, OpportunityEdge.
contractso spine adapters (L1) council (L2) swarm (L3) harness (L4) etl (L0) um vocabulário comum → o resto da stack fica agnóstico de provider
Mudar um tipo aqui muda o contrato de todos os consumidores ao mesmo tempo.
Faça uma aposta antes de continuar

Por que o Alembic usa Result<T,E> em vez de simplesmente throw em erros de IO e parsing?

Para tornar o erro um valor de primeira classe. A convenção do projeto é fail-closed e nunca lançar em código de biblioteca. Devolvendo err(...), o chamador é obrigado pelo tipo a tratar a falha — nada escapa silenciosamente, e o mesmo estilo vale para chamadas de modelo (ModelRunResult).

04

Result e o narrow waist

Dois estilos, uma forma

O Result é uma união discriminada minúscula, sem dependências. Ele espelha de propósito a forma do ModelRunResult, para que os dois leiam igual no ponto de chamada — um para IO/parsing, o outro para chamadas de modelo.

Toda função que pode falhar devolve uma "caixa" com uma etiqueta: ou ok (e o valor dentro) ou err (e o erro dentro). Quem recebe a caixa precisa olhar a etiqueta antes de usar o conteúdo. Erro nunca "vaza" sem ser visto.

packages/contracts/src/result.tsexport interface Ok<T> { readonly ok: true;  readonly value: T; }
export interface Err<E> { readonly ok: false; readonly error: E; }
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 });

// nunca rejeita — embrulha um throw em err()
export const tryCatchAsync = async <T>(fn, onError = toError) => {
  try { return ok(await fn()); }
  catch (cause) { return err(onError(cause)); }
};
council swarm harness marketing-factory ModelAdapter.runModelRunInput → ModelRunResult cliproxyapi MLX local offline fake frontier models
A "cintura estreita": muitos chamadores em cima, muitos provedores embaixo, um único formato no meio.
Por que isto importaComo tudo passa por uma forma só, trocar de provider (gateway, modelo local, fake offline) não muda quem chama. É o que deixa o sistema testável e agnóstico — o tema da lição 2 (Adapters).
05

O pipeline T0

O único tier que sempre roda

O coração do ETL é runT0Pipeline em packages/etl/src/pipeline.ts. Ele percorre o corpus como stream (nunca bufferiza a árvore), e para cada linha .jsonl faz cinco coisas. Clique nas etapas:

Etapa 1

Dedupe por SHA-256

1 · sha256Hex + ledger 2 · validateLlmWikiContract 3 · scorePackage (6 eixos) 4 · priorFor + routesToResidue 5 · scored | residue

O laço, em pseudo-código fiel

packages/etl/src/pipeline.ts — runT0Pipeline// walkCorpus como stream; EXCLUI Repos/Models e Repos/Prompts
for each .jsonl file:
  for each line (readJsonl):
    hash = sha256Hex(line)
    if loadDedupeIndex.has(hash): outcome = 'duplicate'; continue
    recordProcessed(hash)              // ledger append-only
    verdict = validateLlmWikiContract(line)
    if !verdict.ok: outcome = 'invalid'; continue
    score  = scorePackage(pkg, evidence)   // 6 eixos, 0–30
    prior  = priorFor(relativePath)        // família → tier
    if routesToResidue(prior): emit RESIDUE; outcome = 'residue'
    else: outcome = 'scored'

O nome do arquivo de residue é a constante RESIDUE_NAME = '_alembic-residue.jsonl'. Os contadores agregados (filesScanned, scored, residue, duplicates, invalid…) compõem o PipelineStats.

As invariantes do T0

Read-only no source · append-only nos outputs · exclui Repos/Models e Repos/Prompts · 100% determinístico e $0. O fs é injetado (FsPort), então o pipeline inteiro é testável com um filesystem em memória.

Exemplo guiado — o que acontece com uma linha duplicada
1
Chega a linha {...}. Calculamos hash = sha256Hex(line) — a impressão digital do conteúdo.
2
Consultamos o ledger via loadDedupeIndex. O hash já está lá (essa linha veio numa run anterior).
3
Resultado: outcome = 'duplicate'. Não validamos, não pontuamos, não roteamos. duplicates++ e seguimos.
4
Agora você: e se a linha fosse nova mas o contrato falhasse na validação? Qual outcome e até onde o pipeline iria? (Resposta: 'invalid' — para logo após validateLlmWikiContract, sem score nem rota.)
a1f3…novo ✓ 9c02…novo ✓ a1f3…duplicate ✕ 7b41…novo ✓ 9c02…duplicate ✕ _alembic-processed.jsonl (append-only) →
recordProcessed só anexa hashes inéditos; um hash repetido encerra a linha como duplicate.
06

O score de 6 eixos

Pontuar sem chamar modelo nenhum

O scorePackage (packages/etl/src/score.ts) dá uma nota de 0 a 5 em cada um de seis eixos (0 a 30 no total), lendo apenas evidência estrutural do pacote — sem modelo, determinístico, $0. Tiers acima podem reescrever com notas julgadas por modelo; a nota L0 é o piso barato.

Os seis eixos do score L0 em colunas: completeness (chunkCount, cheio em 12, menos contentGaps), accuracy (researchRefs, cheio em 6), clarity (qaPairs, cheio em 8), actionability (understandingChars, cheio em 4000), novelty (understandingChars, cheio em 6000) e provenance (hasRawSource 5 ou 1); cada um de 0 a 5, somando 0 a 30, com a barra de aprovação em 18/30.

Cada eixo vem de uma evidência concreta no pacote. A barra de aprovação padrão é 18/30 (60%).

EixoVem de (evidência)Nota cheia em
completenesschunkCount menos contentGaps12 chunks
accuracyresearchRefs (refs resolvidas)6 refs
clarityqaPairs (pares de Q&A)8 pares
actionabilityunderstandingChars4000 chars
noveltyunderstandingChars6000 chars
provenancehasRawSource resolvido?5 se sim, 1 se não
complete.4 accuracy2 clarity3 actionab.4 novelty2 provenance5 Exemplo: total = 4+2+3+4+2+5 = 20/30 → passa a barra (18)
score.ts — meetsBar(score, 18) verifica o piso; total === soma dos seis eixos (garantido pelo schema).

bandByThreshold e o fail-safe

packages/etl/src/score.ts// converte uma contagem numa banda 0–5 (linear, saturando em 5)
const bandByThreshold = (count, fullMarksAt) =>
  clamp05((count / fullMarksAt) * 5);

// evidência AUSENTE → nota baixa, NUNCA exceção
completeness = clamp05(bandByThreshold(chunkCount ?? 0, 12) - gapPenalty);
provenance   = hasRawSource === true ? 5 : 1;
// pacote bloqueado → zera todos os seis eixos
if (evidence.blocked) return { ...todos 0, total: 0 };

Repare no padrão ?? 0: campo de evidência faltando vira contagem zero e nota baixa. Isso encarna a regra do projeto — não lançar; a ausência é informação, não erro.

O pacote tem hasRawSource = false e chunkCount = 0. Quanto vale provenance?
É 1. A regra é literal: provenance = hasRawSource === true ? 5 : 1. Mesmo sem chunks, o piso é 1, não 0 (o 0 só aparece no caso blocked, que zera todos os eixos de uma vez).
0 fullMarksAt 2× limiar 0 5 linear: (count / fullMarksAt) × 5 satura em 5 (clamp05)
score.ts — uma contagem vira uma banda 0–5; passar do limiar não dá nota extra (clamp em 5).
07

Famílias e roteamento

Quem fica barato e quem sobe

O corpus é organizado em famílias (pelo prefixo do caminho). Cada família tem um prior: o tier-alvo e uma prioridade. Itens cujo prior aponta para um tier acima do piso T0 viram residue — uma fila de trabalho para os tiers caros. O resto é resolvido barato em T0.

FamíliaTier-alvoPrioridadeDestino no T0
TranscriptsT20.9residue ↑
Discord · CircleT20.8residue ↑
SkoolT20.75residue ↑
WhatsappT10.6residue ↑
Bookmarks · SkillsT10.5residue ↑
ReposT10.4residue ↑
UnknownT00.1fica em T0

Valores de FAMILY_PRIORS em packages/etl/src/priors.ts — ajustáveis pelo dono do corpus. Repos/Models e Repos/Prompts nem entram: são excluídos por isExcluded.

priorFor(path) Transcripts → T2 Bookmarks/Skills → T1 Repos → T1 Unknown → T0 residuetiers caros resolvido · $0
routesToResidue(prior) compara o tier-alvo com o piso (T0): acima do piso ⇒ sobe.
CuidadoResidue não é lixo. É exatamente o oposto: é o material valioso o bastante para merecer um tier mais caro. O T0 está dizendo "isto eu não resolvo de graça — deixe um modelo ver".
08

O portão de PII

A barreira de privacidade, fail-closed

Este é um dos gates mais importantes do sistema. Em packages/etl/src/pii.ts, qualquer sinal de um canal privado precisa estar redigido antes de poder ser escrito. O portão é assertRedactedForEmit(signal, channel) e ele nunca lança: falha devolvendo err (fail-closed).

O portão de PII fail-closed: assertRedactedForEmit recebe um sinal e o canal; canais privados (whatsapp, discord, skool, circle) sem redação são barrados com err(private_unredacted) e o sinal é dropado, enquanto canais públicos ou já redigidos seguem como ok(signal) para o store; ao lado, os tokens de redação.

Sinal de canal privado sem redação não passa. Ele é dropado antes de chegar ao store de oportunidade.

Canais privados (PRIVATE_CHANNELS): whatsapp, discord, skool, circle. Sinais dessas famílias passam por redactSignal antes do assert.
Tokens de redação: [REDACTED_EMAIL], [REDACTED_PHONE], [REDACTED_HANDLE], [REDACTED_TOKEN] — o que substitui o dado sensível no texto.
assertRedactedForEmit(signal, channel) já redigido OUcanal público? sim ok(signal)escreve no store não (privado, cru) err(private_unredacted)sinal DROPADO
Fail-closed: na dúvida, não escreve. O gate é chamado por emitSafeSignal em harness/funnel.ts.

O assert + o ponto de chamada

packages/etl/src/pii.tsexport const assertRedactedForEmit = (signal, channel) => {
  const alreadyRedacted = 'redacted' in signal && signal.redacted === true;
  if (alreadyRedacted) return ok(signal);
  if (!isPrivateChannel(channel)) return ok(signal);  // público passa
  return err({ kind: 'private_unredacted', channel, signalId, message });
};
packages/harness/src/funnel.ts — emitSafeSignal// chamado ANTES de qualquer write no store
if (!isPrivateChannel(entry.channel)) {
  const gate = assertRedactedForEmit(entry.signal, entry.channel);
  ...
}
const redacted = redactSignal(entry.signal);     // privado → redige primeiro
const gate = assertRedactedForEmit(redacted, entry.channel);

O gate é o mesmo, mas para canais privados o funnel redige antes e só então afirma. Se mesmo assim a afirmação falhar, o sinal é descartado — nunca chega ao opportunity-graph.

Pense antes de revelar

Um sinal de channel: 'discord' chega sem o campo redacted. O que o portão devolve?

err(private_unredacted). discord é canal privado e o sinal não está redigido — fail-closed. (Na prática o funnel redige antes; mas se você chamar o assert direto com o sinal cru, ele recusa.)
09

Stores content-addressed

Append-only, endereçado por conteúdo

Onde o que sobreviveu ao funnel é guardado: packages/etl/src/stores.ts. Todo write é append-only e endereçado por conteúdo — a chave de dedupe é o hash do próprio registro. Re-anexar conteúdo idêntico é um no-op. Os writes são atômicos (temp-write + rename via FsPort.writeFileAtomic).

registro{kind, payload} canonicalJsonchaves ordenadas sha256Hex64 hex chave dededupe dois registros estruturalmente iguais → mesmo hash → um só no disco
contentHash = SHA-256 sobre o JSON canônico (chaves ordenadas recursivamente).
OPPORTUNITY_GRAPH_PATH = Business/opportunity-graph.jsonl — sinais e arestas de oportunidade.
LEARNINGS_PATH = Skills/learning/learnings.jsonl — aprendizados acumulados.
Detalhe finoQuando um registro carrega um campo volátil (ex.: um timestamp) mas tem identidade estável, passa-se um hashOf customizado para o dedupe convergir na identidade, não no carimbo de tempo. Assim re-runs não acumulam quase-duplicatas. O hashOf deve devolver 64 chars hex minúsculos.
10

Recuperação ativa

Vire os cartões e tente lembrar primeiro

Conceito
O que é a "cintura estreita"?
clique para virar
ModelRunInput → ModelRunResult em ModelAdapter.run: a forma única por onde TODA chamada de modelo passa. Muitos chamadores acima, muitos provedores abaixo.
T0
Três invariantes do pipeline T0?
clique para virar
Read-only no source · append-only nos outputs · 100% determinístico e $0 (e exclui Repos/Models e Repos/Prompts).
Score
Quanto vale o score no total e qual a barra?
clique para virar
6 eixos × 0–5 = 0 a 30. A barra padrão de meetsBar é 18/30 (60%).
PII
Quais são os 4 canais privados?
clique para virar
whatsapp, discord, skool, circle. Sem redação → err(private_unredacted), sinal dropado.
11

Recapitulando

A camada L0 em cinco slides

A grande ideia

Barato e seguro primeiro

L0 = contracts (os tipos) + ETL T0 (o pipeline gratuito). Tudo que chega caro já passou por aqui.

T3/T4 T1/T2 L0 · T0 ($0)
1

Contracts

A cintura estreita

Result para IO/parsing e ModelRunResult para modelos — uma forma que toda a stack respeita, e que deixa o sistema agnóstico de provider.

ok: true · value ok: false · error discriminado em `ok`
2

T0 pipeline

Cinco etapas, $0

dedupe SHA-256 → validar contrato → score de 6 eixos → rotear por prior → emitir scored ou residue. Determinístico, stream, FsPort injetável.

1 2 3 4 5
3

Roteamento

Residue é o valioso

Transcripts→T2, Bookmarks/Repos→T1, Unknown fica em T0. O que aponta acima do piso vira residue para os tiers caros.

piso T0 T0 T1 T2 → residue
4

Privacidade

O portão fail-closed

assertRedactedForEmit: canal privado sem redação → err, sinal dropado. Stores append-only, endereçados por conteúdo.

privado cru gate err → dropado redigido → ok → store
5
Slide 1 / 5 Use
Em uma frase, para você: "A camada L0 mantém o Alembic ____ e ____, porque os contratos definem ____ e o T0 ____ antes de qualquer modelo caro." Se você preenche as quatro lacunas, está pronto para a lição 2.
12

Verifique

Prove que a camada faz o que dizemos

A melhor coisa para abrir primeiro
packages/etl/src/pipeline.ts — leia o comentário de cabeçalho de runT0Pipeline e o laço. Depois score.ts e pii.ts. É a fonte de verdade desta lição.

Testes e um T0 real

# foco nos testes da camada
pnpm --filter @alembic/etl test
# pipeline.test.ts · pii.test.ts · stores.test.ts · score

# um T0 real, offline ($0), inspecionando saídas
alembic distill ./corpus --offline --dataDir /tmp/test0
cat /tmp/test0/_alembic-residue.jsonl | head -5
wc -l /tmp/test0/_alembic-processed.jsonl   # o ledger
Revisão pontuada

Responda às três para fechar a lição.

Qual afirmação sobre o T0 é verdadeira?
É a (c). T0 é 100% determinístico e gratuito, read-only no source e append-only nos outputs. Ele não chama modelo nenhum — esse é justamente o ponto: ser o filtro barato antes dos tiers caros.
Um item da família Transcripts chega ao T0. O que acontece?
É a (a). Transcripts tem prior de tier T2 (prioridade 0.9). Como T2 está acima do piso T0, routesToResidue é verdadeiro e o item sobe via _alembic-residue.jsonl.
Por que assertRedactedForEmit devolve err em vez de lançar?
É a (b). A convenção do projeto é nunca lançar em biblioteca: o erro vira valor (Result/err), o chamador é obrigado a tratá-lo, e na dúvida o sinal não é escrito — privacidade fail-closed.
Acertos: 0/3
As dez verdades da L0
  1. L0 = contracts (os tipos) + ETL T0 (o pipeline gratuito).
  2. A cintura estreita é ModelRunInput → ModelRunResult; tudo passa por ela.
  3. Result<T,E> torna o erro um valor — nunca throw em biblioteca.
  4. T0 é determinístico, $0, read-only no source, append-only nos outputs.
  5. Cinco etapas: dedupe → validar → score → rotear → emitir.
  6. Dedupe é por SHA-256 contra um ledger append-only.
  7. O score tem 6 eixos (0–5 cada, 0–30), a barra é 18/30.
  8. Evidência ausente vira nota baixa, nunca exceção.
  9. Residue = o material valioso o bastante para um tier caro.
  10. O portão de PII é fail-closed: privado sem redação → dropado.
Ainda em dúvida sobre por que a cintura estreita deixa o sistema agnóstico de provider? Segure essa pergunta — a lição 2 · Camada L1 · Adapters mostra exatamente quem implementa ModelAdapter.run e como o adapter offline, o gateway e os modelos locais entram pela mesma porta.