Curso / Lição 15
Lição 15 · Motor & método

O funil: um ETL de 4 camadas com piso de $0

O funil é como o Alembic transforma um corpus de fontes brutas em sinais de negócio validados e learnings — a um custo que começa em exatamente $0 e só sobe à medida que candidatos provam valer o gasto. É uma cascata de quatro camadas: T0 determinística (grátis) → T1 local (~grátis) → T2 shortlist de fronteira medida → T3 council + painel verificador. Só um GO verificado emite. É o motor de dados do qual o ciclo de aprendizado se alimenta.

Leia primeiro (fonte primária)
Alembic — packages/harness/src/funnel.ts + packages/etl/src/*

Esta lição destila o orquestrador do funil e os módulos de ETL que ele compõe, lidos literalmente do repositório (rodapé). Por que importa pra missão: é o coração econômico do Alembic — a peça que deixa rodar a destilação inteira de um corpus enorme por $0 e só pagar fronteira pelo que sobreviveu a dois filtros mais baratos.

Objetivos desta lição
  • Explicar por que o funil estreita o gasto e não o corpus — a curva de custo verga na direção certa.
  • Nomear o que cada camada faz: runT0PipelinerunT1ExtractionrunT2ShortlistrunT3Council.
  • Descrever o GO-verificado como a conjunção de duas travas: consenso GO e isPanelEmissionApproved.
  • Defender as três invariantes que o funil nunca pode regredir: PII antes de egress, orçamento fail-closed, append-only.
0
camadas · T0 → T3
$0
piso de custo (T0 toca 100%)
0
membros do council (quórum MIN_VALID_AGENTS)
0
travas no GO-verificado

01 · A cascata — estreite o gasto, não o corpus

A camada mais barata toca 100% do corpus; cada camada seguinte é mais cara mas vê apenas os sobreviventes da anterior. Esse é o truque que faz a curva de custo vergar na direção certa: você nunca paga fronteira por um item que um filtro de $0 já podia descartar.

It drives one corpus through four tiers over materialized wiki packages.packages/harness/src/funnel.ts, docstring do orquestrador (55–81)

O funil como funil — largura = quantos itens, fundo = quanto custa

Desenhe-o literalmente como um funil. A boca larga em cima é barata e vê tudo; o bico estreito embaixo é caro e vê pouco. A largura de cada faixa é quantos itens passam; a etiqueta à direita é o que aquela faixa custa:

O FUNIL · da boca larga ($0, 100% do corpus) ao bico estreito (medido, só o que sobreviveu)
T0 · walk determinístico → dedupe SHA-256 → valida contrato → score 6-dim → resíduo 100% do corpus (exclui Repos/Models + Repos/Prompts) $0 T1 · um BusinessSignal por item de resíduo · adapter LOCAL (free tier) só o resíduo que T0 roteou para cima ~$0 T2 · shortlist FRONTIER medida refina os T1 mais fortes, em lotes medido T3 · council + painel N-lentes medido só um GO-verificado (GO ∧ painel-aprovado) sai pelo bico e emite

A curva de custo verga: barato sobre muitos, caro sobre poucos

Por que isso é barato no agregado? Porque o produto itens × preço-por-item é mínimo em cada faixa. A faixa que vê milhares de itens custa $0 cada; a faixa que custa caro cada vê pouquíssimos. Veja as duas curvas cruzando:

VOLUME (cai) × PREÇO-POR-ITEM (sobe) · o gasto total é a área pequena onde os dois se encontram
T0 → T1 → T2 → T3 (camadas) alto volume de itens ↓ preço por item ↑ caro só toca poucos o gasto agregado = Σ (itens × preço) é pequeno: a faixa cara nunca vê o volume cheio.
A frase-chave
Não é "estreite o corpus", é "estreite o gasto". T0 ainda processa o corpus inteiro — só que de graça. O que afunila é quanto dinheiro cada item pode custar antes de provar que merece a próxima camada.

02 · Cada camada, por dentro

As quatro camadas não são abstratas: cada uma é uma função real em funnel.ts (T1–T3) ou em @alembic/etl (T0). Primeiro a visão simples, depois a tabela técnica com os nomes verbatim.

Em uma frase cada:
T0 — "leia tudo de graça e jogue o lixo fora": anda no corpus, remove duplicatas, valida o formato, dá uma nota e separa o que merece atenção (o resíduo).
T1 — "transforme cada sobrevivente num sinal", usando um modelo local (custo ~zero).
T2 — "pegue os sinais mais fortes e mande para um modelo de fronteira" (aqui sim você paga, mas só pelos melhores).
T3 — "submeta a um comitê e a um verificador independente": só o que ambos aprovam vira oportunidade.
Os nomes reais (de funnel.ts): runT0Pipeline (de @alembic/etl, importado) → runT1ExtractionrunT2ShortlistrunT3Council. O registry de adapters é injetado (FunnelOptions.adapters): um registry offline torna a run inteira $0 e hermética (sem rede). A saída é o FunnelReport, com contadores por camada + costUsd + verifiedSignals.

T0 por dentro — cinco passos determinísticos, custo zero

A camada grátis não é mágica: é uma esteira de cinco passos puros, sem nenhum modelo. É justamente por não chamar LLM que ela pode rodar sobre 100% do corpus a $0. Siga a esteira:

runT0Pipeline · walk → dedupe → valida → score → resíduo (nenhum modelo, $0)
walk walkCorpus dedupe SHA-256 valida contrato wiki score 6 dimensões resíduo → o que sobe p/ T1 Determinístico = mesmo corpus, mesmo resíduo. Nenhuma chamada de modelo aqui → por isso $0 sobre 100%.

O que é o "score 6-dim" — as seis dimensões de T0

Aquele passo score não dá uma nota única arbitrária: o scorer determinístico de L0 pontua cada pacote em seis eixos, de 0 a 5 cada, somando 0 a 30. É essa nota que o prior usa para decidir se um item vira resíduo (sobe) ou fica no piso. Veja os seis eixos e a barra de corte:

SCORE 6-DIM · completeness · accuracy · clarity · actionability · novelty · provenance (0–5 cada, 0–30 total)
completeness4 / 5 accuracy5 / 5 clarity4 / 5 actionability3 / 5 novelty2 / 5 provenance5 / 5 soma = 23 / 30 · acima da barra meetsBar (≥ 18) → o prior pode rotear este item para o resíduo (exemplo ilustrativo das notas).
Por que isto é determinístico: cada eixo sai de uma evidência objetiva do pacote (understandingChars, researchRefs, qaPairs, hasRawSource …), não de um modelo. Mesmo corpus → mesmas seis notas → mesmo resíduo. provenance é binário-ish (hasRawSource ? 5 : 1): sem ponteiro de fonte resolvido, o item perde quase todo o eixo. As notas deste gráfico são um exemplo; o que é fato do código é quais são os seis eixos, a faixa 0–5/0–30 e a barra meetsBar em 18.
CamadaFunçãoO que fazCusto
T0runT0Pipelinewalk → dedupe SHA-256 → valida contrato → score 6-dim → emite resíduo, sobre 100% do corpus (excluindo Repos/Models + Repos/Prompts)$0
T1runT1Extractionum BusinessSignal por item de resíduo via o adapter LOCAL injetado; free-tier, então na prática nunca bloqueado por orçamento~$0
T2runT2Shortlistfiltra por strength >= shortlistMinStrength e refina os sinais T1 mais fortes em lotes via um modelo FRONTIER; toda chamada paga é medidamedido
T3runT3Councilum council sintético de 3 membros (optimist/analyst/pessimist, atende MIN_VALID_AGENTS) + o painel verificador N-lentesmedido

O fluxo de dados T0 → T1 → T2 → T3

Como o dado muda de forma ao descer? Cada camada recebe a saída tipada da anterior e a estreita. Siga as setas — repare como o tipo some de "arquivos" para "resíduo" para "sinais" para "sinais verificados":

PIPELINE DE DADOS · cada caixa transforma e estreita a saída da anterior
T0 corpus → resíduo T1 resíduo → BusinessSignal[] T2 sinais → shortlist T3 shortlist → verifiedSignals Tipos reais: PipelineStats (T0) · BusinessSignal[] (T1/T2) · arestas + learnings + verifiedSignals (T3). Fonte: funnel.ts — T1 (381–410), T2 shortlist (423–450), T3 council (477–514).

03 · O custo por camada — lado a lado

O ponto inteiro do funil é o custo. Veja-o explícito, camada a camada: quem paga, quem vê quantos itens e o que protege cada chamada paga. A tabela diz os fatos; o gráfico abaixo torna o contraste visível.

CamadaItens que vêModeloCusto/chamadaProteção
T0100% do corpusnenhum (determinístico)$0
T1só o resíduo de T0LOCAL (free-tier)~$0BudgetGuard.check (sempre passa em $0)
T2só os T1 mais fortesFRONTIERmedidoBudgetGuard.check fail-closed por lote
T3só a shortlistFRONTIER ×3 (+ painel)medidoBudgetGuard.check por membro

O gráfico empilha as duas dimensões que importam: a cor da etiqueta é quem paga; a largura é a fatia do corpus que cada camada toca. Repare como o que custa é justamente o que estreitou:

CUSTO × ALCANCE · grátis vê tudo (verde, largo) · medido vê pouco (clay, estreito)
T0 · $0 vê 100% do corpus — de graça T1 · ~$0 vê só o resíduo — local, free-tier T2 · medido só os T1 mais fortes T3 · medido só a shortlist

04 · Calcule o gasto você mesmo (slider)

Sinta a economia. Arraste o tamanho do corpus: as barras mostram quantos itens cada camada toca, e o painel calcula o gasto agregado. Repare como T0/T1 ficam em $0 e o total só cresce com as poucas chamadas pagas de T2/T3 — e como o offline zera tudo.

T010000 · $0 T13000 · ~$0 T2300 · medido T330 · medido Taxas de sobrevivência ilustrativas (T0→T1 30%, T1→T2 10%, T2→T3 10%) · preços ilustrativos de fronteira. As proporções e o $0 dos tiers locais são reais; os números absolutos são uma estimativa de demonstração.

[uncertain] as taxas de sobrevivência e o preço por chamada são valores de demonstração (não há constantes fixas no código — dependem de shortlistMinStrength, do corpus e da tabela de preços do registry). O que é fato do código: T0/T1 custam $0/~$0 e --offline zera o total.

Por que offline zera tudo: --offline injeta um registry de adapters offline; createBudgetGuard trata qualquer chamada projetada em $0 como sempre-aprovada, então nenhuma camada gasta. Por isso alembic distill <corpus> --offline roda o pipeline inteiro, hermético, a custo zero — o piso do design, não um modo degradado.

05 · O sinal GO-verificado — duas travas, não uma

Chegar ao T3 não basta para emitir. Um resultado só vira oportunidade quando ambos são verdade: a decisão de consenso é GO e o painel N-lentes aprovou a emissão (verificado, não estacionado). Um GO puro não basta:

// packages/harness/src/funnel.ts:509-517 — o portão de emissão (forma do código)
if (!isPanelEmissionApproved(report) || consensus.decision !== 'GO') {
  return { verified: 0, edges: [], learnings: [], signals: [] };  // nada emite
}
// caso contrário: emite arestas + learnings; expõe os sinais PII-safe em verifiedSignals

Lido na forma positiva: emite ⟺ consensus.decision === 'GO' && isPanelEmissionApproved(report). Qualquer trava que falhe → o early-return devolve zero e nada chega ao store.

As duas travas como um portão AND

Pense em duas chaves girando a mesma fechadura: a porta só abre se as duas girarem. Uma sozinha não emite nada:

PORTÃO AND · só GO ∧ painel-aprovado abre a emissão (qualquer falha → estacionado)
trava 1 · consensus decision === 'GO' trava 2 · painel N-lentes isPanelEmissionApproved AND ✓ EMITE arestas + learnings + verifiedSignals qualquer entrada falsa → verified: 0 (estacionado, nada emite)

Os BusinessSignal[] GO-verificados são expostos em FunnelReport.verifiedSignals — a ponte PII-safe para a marketing factory (distillAndMarket). A Lição 18 abre o painel por dentro; o ponto aqui é que o funil exige consenso e verificação independente antes de gastar esforço a jusante ou emitir qualquer coisa para fora.

Dentro da trava 1 — três votos, e o quórum que tolera faltas

Como o consenso vira GO? O runT3Council despacha um council sintético fixo de três membrosoptimist, analyst, pessimist — contra o adapter de fronteira de T3, com budget-gate em cada chamada. Aqui mora um detalhe fail-closed elegante: se o orçamento bloqueia um membro (ou o parse do voto falha), aquele voto é simplesmente omitido — a run não quebra. Quem decide se ainda há consenso é o quórum MIN_VALID_AGENTS = 3:

COUNCIL T3 · 3 membros votam · budget-block omite um voto · validVoteCount ≥ MIN_VALID_AGENTS
optimist budget.check ✓ analyst budget.check ✓ pessimist budget-block → omitido (sem voto) agregador fail-closed conta validVoteCount validVoteCount ≥ 3 ? 2 < 3 → não provado (sem GO) Quórum não atingido (só 2 votos válidos) → a decisão NÃO é um GO verificado. Fonte: funnel.ts (529–560), consensus.ts:27 (MIN_VALID_AGENTS=3), verifier.ts (123–124).
Repare na elegância fail-closed: orçamento estourado não vira exceção — vira menos votos. E menos votos que o quórum vira não-GO, não um GO frágil. O sistema degrada para o lado seguro: na dúvida (votos insuficientes), não emite. É a invariante ② (orçamento fail-closed) e a trava 1 (consenso) trabalhando juntas — um caso em que "degrada" e "fecha o portão" são a mesma coisa.
Preveja antes de continuar
O consenso do council deu GO, mas o painel N-lentes estacionou uma lente. Quantos sinais o funil emite para verifiedSignals neste caso?
Zero. O portão é um AND: isPanelEmissionApproved(report) === false dispara o early-return { verified: 0, …, signals: [] }. Não importa que o consenso tenha sido GO — uma trava falha derruba a emissão inteira. Se você chutou "emite com flag de aviso", caiu na intuição errada: o funil não emite-com-ressalva, ele não emite. Consenso e verificação são travas independentes, e a porta só abre com as duas.

06 · Fluxograma — "esse sinal vai emitir?"

Junte tudo numa única decisão. Diante de um item do corpus, siga as setas: cada losango é uma pergunta que escolhe o caminho, do $0 de T0 até o portão final. Os caminhos de descarte (PII, orçamento, painel) são tão importantes quanto o caminho de emissão.

FLUXOGRAMA · do item bruto ao GO-verificado (cada losango é uma pergunta)
item do corpus T0: dedupe + valida contrato roteia p/ resíduo? (tier > T0) NÃO fica no piso ($0) SIM canal privado? → redige extractionInput (antes do modelo) T1: orçamento permite a chamada? NÃO budget-blocked degrada (não estoura) SIM ($0 local) T2: forte o bastante? strength ≥ shortlistMinStrength T3: GO ∧ painel aprovado? NÃO ✗ estacionado verified: 0 SIM ✓ EMITE → verifiedSignals (PII-safe)
Por que a redação vem ANTES de T1 (e não na emissão): extractionInput redige o conteúdo de canal privado antes da chamada de modelo, para que PII nunca chegue a um adapter (possivelmente em rede) intacta. A emissão é re-checada por assertRedactedForEmit — duas barreiras, não uma.
Por que "degrada" e não "quebra": um budget.check negativo apenas pula a chamada paga (T1 conta como budgetBlocked; T2/T3 mantêm o sinal sem refino). A run continua e termina — o orçamento é um teto, não uma exceção que mata o processo.

07 · Três invariantes que o funil nunca pode regredir

O docstring do funnel.ts chama três invariantes de "load-bearing, do not regress". São as garantias que tornam o funil seguro o suficiente para rodar sozinho sobre um corpus privado.

① PII antes de egress

Um sinal de um canal PRIVADO (whatsapp, discord, skool, circle — a constante PRIVATE_CHANNELS) é redigido antes da chamada de modelo (extractionInput chama redactPii) e protegido de novo por assertRedactedForEmit antes de qualquer escrita (emitSafeSignal). Um sinal de canal privado não-redigido é descartado, nunca emitido. FunnelReport.t1PiiBlocked deveria ser sempre 0 — um valor não-zero é um alarme fail-closed. Governado pela ADR-0011.

② Orçamento fail-closed

Toda chamada paga (T2/T3, e a verificação de T1) passa por um BudgetGuard.check fail-closed antes do despacho; uma violação projetada bloqueia a chamada e a camada degrada em vez de estourar o gasto. A precificação sempre usa a tarifa de tier do registry (TierRoute.pricingModelId), nunca um override de catálogo — então um modelo de gateway sobrescrito que não está no registry ainda é medido contra o teto pela tarifa do seu tier. Você não consegue contornar o orçamento por acidente.

③ Append-only

Resultados fluem para os dois stores via escritas append-only, content-addressed, atômicas, validadas por schema; leituras da fonte permanecem read-only. As duas saídas são o grafo de oportunidades BUSINESS (Business/opportunity-graph.jsonl) e o store de LEARNINGS (Skills/learning/learnings.jsonl) — as duas cadeias de valor da ADR-0002.

A barreira de PII em duas etapas

A invariante ① é a mais sutil: a redação não acontece uma vez, mas duas. Veja o caminho de um sinal de WhatsApp passando pelas duas barreiras — e o que acontece se ele chegar cru à segunda:

PII · barreira 1 (antes do modelo) + barreira 2 (antes da escrita) — fail-closed
sinal privado channel=whatsapp barreira 1 extractionInput redactPii (antes do modelo) barreira 2 assertRedactedForEmit (antes da escrita) ✓ redigido emite ✗ cru descarta + t1PiiBlocked++ Canal público: barreira 1 envia verbatim (sem PII forçada); barreira 2 ainda valida. Fonte: pii.ts (24–29), funnel.ts (185–197, 246–256).

Append-only: quatro garantias numa escrita

A invariante ③ empacota quatro propriedades numa só escrita. Cada uma fecha uma classe de bug — corrupção, duplicata, leitura parcial, registro malformado:

A ESCRITA DOS STORES · quatro garantias em série antes de uma linha existir
1 · VALIDATED Zod schema ou rejeita 2 · CONTENT-ADDR SHA-256 → dedupe 3 · ATOMIC writeFileAtomic 4 · APPEND-ONLY linha existe (nunca reescrita) Stores: Business/opportunity-graph.jsonl + Skills/learning/learnings.jsonl. A fonte é lida read-only. Fonte: stores.ts (26–37) · ADR-0002 (as duas cadeias de valor).
Exemplo resolvido · um sinal de WhatsApp tenta emitir, ainda não-redigido
1
Origem. O item vem de um canal em PRIVATE_CHANNELS (whatsapp). Logo, sujeito a redação forçada.
2
Barreira 1 (T1). extractionInput roda redactPii(item.evidence) antes de montar a entrada do modelo. No caminho normal, o modelo nunca vê PII.
3
Barreira 2 (emissão). Suponha que, por um bug, o sinal chegue cru à emissão. emitSafeSignal chama assertRedactedForEmit(signal, channel).
4
Resultado. A asserção devolve err (nunca lança), emitSafeSignal retorna undefined → o sinal é filtrado fora de verifiedSignals, e t1PiiBlocked incrementa como alarme.
Agora você: e um sinal de canal público (ex.: um bookmark)? Passa por assertRedactedForEmit? Pense antes de revelar.
Sim — emitSafeSignal chama o gate também para canais públicos (ramo !isPrivateChannel), só que sem forçar redação antes. O gate ainda valida; a diferença é que conteúdo público é enviado verbatim ao modelo na barreira 1. Lição: a segunda barreira é universal; a primeira (redação forçada) é só para canais privados.

08 · Por que o funil vive no harness L4, e não no etl

Uma decisão de arquitetura fecha a lição. O funil orquestra três camadas: adapters L1 + council L2 + etl L0. Onde, então, ele deve morar? A resposta vem de uma regra de dependência, não de gosto.

Colocá-lo em etl forçaria o etl a depender para cima de adapters/council, invertendo o grafo de camadas. Então a T0 determinística fica em @alembic/etl (puro, só-contracts) e o orquestrador que chama T0→T3 vive em @alembic/harness:

GRAFO DE CAMADAS · o funil (L4) aponta para baixo; etl (L0) jamais aponta para cima
funnel · @alembic/harness (L4) o orquestrador T0→T3 adapters (L1) modelos T1/T2/T3 council (L2) consenso + painel etl (L0) runT0Pipeline (puro) setas só descem (verde, OK). Pôr o funil no etl criaria uma seta L0 → L1/L2 (vermelha) — proibida. ✗ inversão
A regra em uma linha: a estratificação é preservada por onde o código fica, não por convenção. T0 puro embaixo (etl), orquestrador no topo (harness). Quem inverte a seta quebra o grafo — e o build de camadas o pega.

09 · O que o funil te devolve — anatomia do FunnelReport

Uma run inteira do funil produz um objeto: o FunnelReport. Ele é a prova auditável do que aconteceu em cada camada — não só o que saiu, mas onde as coisas pararam e quanto custou. Ler este contrato é ler o funil de relance. Os campos não são decorativos: três deles são alarmes de invariante.

FunnelReport · contadores por camada + custo + os sinais verificados (forma do tipo, funnel.ts:93–127)
t0: PipelineStats files · scored · residue … T1 t1Extracted t1PiiBlocked ⚠ t1BudgetBlocked T2 t2Shortlisted t2BudgetBlocked t3Verified debates que chegaram a GO opportunityRecordsWritten learningsWritten costUsd · total medido dryRun (sem escritas) verifiedSignals BusinessSignal[] GO-verificado + PII-safe → a ponte para distillAndMarket Os três alarmes (deveriam ser baixos/zero numa run saudável): t1PiiBlocked — deveria ser 0. Não-zero = redação foi contornada → invariante ① violada (fail-closed, alertar). t1BudgetBlocked / t2BudgetBlocked — chamadas puladas pelo orçamento. Alto = teto baixo demais; a camada degradou (não estourou). t3Verified vs opportunityRecordsWritten — quantos chegaram ao GO vs quantos viraram linha no store (em dryRun, escreve 0). O relatório é a auditoria da run: cada item do corpus está contado em exatamente uma camada de saída. Fonte: funnel.ts:93–127.
Leia o report como um funil invertido: t0.residue > t1Extracted > t2Shortlisted > t3Verified. Se algum degrau não estreita, ou o corpus é minúsculo, ou um filtro não está discriminando. A forma do report é a forma do funil.
Por que costUsd mora no report (e não num log à parte): custo é um resultado de primeira classe da run, não um efeito colateral. Ele soma toda chamada paga (T2/T3 + a verificação de T1) medida pela tarifa de tier — então o número que você lê é o número que o BudgetGuard protegeu. Auditável de ponta a ponta.

10 · Como isso se encaixa

O funil não é uma peça isolada — é o motor de dados que fica entre as fontes brutas e o ciclo de aprendizado. Olhe a peça inteira: fontes brutas entram por ingest/wiki packages, descem os quatro tiers T0→T3 (esta lição), e só um GO-verificado sai pelo bico para virar learnings que realimentam o loop. As outras peças da metodologia ou alimentam o funil ou consomem o que ele emite.

ONDE O FUNIL VIVE · fontes brutas → ingest → T0→T1→T2→T3 (esta peça) → GO-verificado → learnings realimentam o loop
fontes brutas repos · transcripts · bookmarks ingest → wiki pacotes materializados O FUNIL · esta lição (harness L4) T0 · determinístico · $0 · 100% do corpus T1 · local · ~$0 · só o resíduo T2 · frontier medido · shortlist T3 · council + painel · medido o gasto afunila, o corpus não GO-verificado (GO ∧ painel) verifiedSignals PII-safe + arestas learnings + oportunidades stores append-only (ADR-0002) realimenta o loop A seta tracejada fecha o ciclo: o que o funil aprende vira prior da próxima destilação. Setas verdes = só emite com prova; tracejada = realimentação.
Clique para acender peça por peça — das fontes ao loop.
Onde você está na metodologia: o funil é a etapa de destilação — depois da ingestão (que materializa wiki packages) e antes do consumo (a marketing factory lê verifiedSignals; o loop de aprendizado lê os learnings). Ele reusa peças que outras lições abrem por dentro: o council/painel de T3 é o mesmo gate do runtime de execução. Veja o todo no mapa interativo da metodologia.

As peças que se conectam a esta

O funil toca cinco outras lições — cada uma abre uma peça que aqui aparece de relance:

Lição 16 · As quatro invariantes
Porque conecta: as três invariantes que o funil "nunca pode regredir" (PII antes de egress, orçamento fail-closed, append-only) são a aplicação concreta das invariantes do sistema — a Lição 16 generaliza o que aqui você viu fechar o portão.
Lição 18 · Council & Verifier
Porque conecta: a trava 1 do GO-verificado (o council de 3 membros) e a trava 2 (o painel N-lentes / isPanelEmissionApproved) são exatamente a peça que a Lição 18 abre por dentro — T3 é onde o funil chama esse council.
Lição 26 · Proveniência & segurança
Porque conecta: a barreira de PII em duas etapas (redactPii antes do modelo + assertRedactedForEmit antes da escrita) é a regra de segurança que a Lição 26 trata em profundidade — aqui ela é a invariante ① do funil.
Lição 27 · Tiers, custo & orçamento
Porque conecta: os tiers T0–T3 e o BudgetGuard fail-closed que mede cada chamada paga pela tarifa de tier do registry são o mesmo modelo de custo que a Lição 27 detalha — o funil é a aplicação econômica desse roteamento.
Lição 17 · A pipeline de gates
Porque conecta: o portão AND do funil (GO ∧ painel-aprovado) é o mesmo padrão fail-closed dos gates de execução (Scope → Council → Proof → Validator) — a Lição 17 mostra esse padrão no caminho de build, este aqui mostra no caminho de destilação.

11 · Na prática

Chega de teoria — rode o funil. A camada local é o piso do design, então a forma canônica de exercitá-lo é offline: o pipeline inteiro, hermético, a custo $0. O comando é alembic distill; alembic status mostra o estado dos stores que o funil alimenta.

# O funil noturno, offline = $0 hermético (sem rede, determinístico)
# <corpus> = uma família wiki, um pai multi-família, ou um arquivo de records
$ alembic distill ~/Documents/Resources --offline

# saída esperada (forma ilustrativa — os números dependem do seu corpus):
#   T0  files=12480  scored=12480  residue=3120     cost=$0
#   T1  extracted=3120  piiBlocked=0  budgetBlocked=0  cost=$0
#   T2  shortlisted=312  budgetBlocked=0             cost=$0   (offline)
#   T3  verified=28   opportunities=28  learnings=28  cost=$0   (offline)
#   total costUsd=$0.00

# Depois: veja o estado dos stores que o funil alimenta
$ alembic status
O que ler na saída: os três alarmes da Lição (seção 09) aparecem aqui — t1PiiBlocked deve ser 0 (não-zero = invariante ① violada); budgetBlocked alto = teto baixo demais; e a contagem deve estreitar degrau a degrau (residue > extracted > shortlisted > verified). Em --offline todo cost é $0 por construção. [uncertain] os números acima são ilustrativos — a forma exata da saída depende da build do CLI; o que é fato é o comando e que --offline zera o custo.
Experimente · rode o funil offline em ~2 minutos
1
Entre no repo e garanta o build. cd /Users/acf/Documents/Projects/appfy/alembic e então pnpm -r build — o CLI alembic precisa estar compilado.
2
Aponte para um corpus. Use uma família wiki real (ex.: ~/Documents/Resources) ou qualquer pasta com pacotes materializados. <corpus> aceita o mesmo caminho que distill documenta: família, pai multi-família, ou um arquivo de records.
3
Rode offline. alembic distill <corpus> --offline. Como é offline, é hermético e $0 — pode rodar sem o gateway cliproxyapi e sem token.
4
Leia o relatório. Procure as contagens por camada (T0→T3) e confirme que costUsd é $0. Esse objeto é o FunnelReport da seção 09 — a forma do report é a forma do funil.
5
Veja os stores. alembic status mostra o estado das duas saídas append-only (oportunidades + learnings) que o funil alimenta.
Agora você: sem mudar o corpus, o que você espera que aconteça com costUsd se tirar o --offline (e o gateway estiver disponível)? Pense antes de revelar.
Só T2/T3 passam a custar (e a verificação de T1) — T0/T1 continuam $0/~$0. O total sobe, mas é medido pela tarifa de tier do registry e protegido pelo BudgetGuard fail-closed: o número final é o número que o guard deixou passar. Lição: tirar o offline não "liga tudo" — liga só as poucas chamadas de fronteira que sobreviveram aos filtros de $0.
Comandos vizinhos (mesma família de dados): alembic ingest <source> materializa novas fontes antes do funil; alembic distill <corpus> --offline roda os quatro tiers; alembic status inspeciona os stores depois. Para a teoria de custo por trás do --offline, volte à seção 04 (o slider) e à Lição 27.

Confusões comuns

"Offline significa resultados degradados." Para T0/T1 significa hermético, determinístico e $0 — as camadas locais são o piso do design, não um fallback. alembic distill <corpus> --offline roda o pipeline inteiro com um registry de adapters offline e nunca toca uma API paga.
"O teto de orçamento pode ser contornado com um override de modelo." Não — a precificação sempre resolve pela tarifa de tier do registry (pricingModelId), então até um modelo de gateway sobrescrito no catálogo é medido pelo seu tier. O budget guard vê o custo projetado independentemente de qual nome de modelo concreto foi fixado.
"Chegou ao T3, então vai emitir." Não — T3 é onde a decisão acontece, não onde a emissão é garantida. Um consenso NO_GO, um painel estacionado, ou um votes.length === 0 (todos os membros bloqueados por orçamento) devolvem verified: 0 e nada emite.

Fixe os conceitos (flashcards)

Clique pra virar. Tente lembrar a resposta antes de virar — recuperação ativa fixa mais que reler.

Cascata
O funil estreita o quê?
clique pra virar ↻
Resposta
O gasto, não o corpus. T0 ($0) toca 100%; só os sobreviventes chegam às camadas pagas T2/T3.
Camadas
As 4 funções, em ordem?
clique pra virar ↻
Resposta
runT0PipelinerunT1ExtractionrunT2ShortlistrunT3Council.
GO-verificado
Quais as duas travas para emitir?
clique pra virar ↻
Resposta
Consenso decision === 'GO' E isPanelEmissionApproved(report). Um AND: qualquer falha → verified: 0.
Invariantes
As 3 que nunca regridem?
clique pra virar ↻
Resposta
① PII antes de egress (t1PiiBlocked=0) · ② orçamento fail-closed (pricingModelId) · ③ append-only (ADR-0002).

Revisão cumulativa — recupere de memória

Antes de clicar: responda de cabeça. As quatro opções têm o mesmo tamanho de propósito — sem pista pela forma.

1. Um corpus tem 10.000 itens. Aproximadamente quantos chegam à camada paga T2?
Correto: b — T0 (grátis) toca 100%; T1 (local free-tier) extrai um sinal por item de resíduo; T2 (medida) filtra por strength ≥ shortlistMinStrength e refina só os mais fortes em lotes. a confunde T0 (que de fato vê tudo, de graça) com as camadas pagas — o funil estreita o gasto, não deixa toda camada ver tudo. c troca o quórum do council (3 membros) pela contagem de itens de T2 — coisas diferentes. d é falso: T2 roda sempre que há sinais fortes e orçamento; só é pulada se nenhum sinal atinge o limite ou o budget bloqueia.
2. O consenso é GO mas o painel N-lentes estacionou uma lente. O funil emite o sinal?
Correto: d — o GO-verificado é a conjunção de duas travas independentes; o early-return dispara se qualquer uma falha, devolvendo signals: []. a trata GO como suficiente, ignorando a segunda trava (o painel) — exatamente o erro que o portão AND existe para impedir. b inventa um "emite-com-aviso" que o código não tem: ou emite limpo, ou não emite. c mistura camadas: orçamento decide se a chamada roda, não se um GO-mais-painel-estacionado emite.
3. Um sinal de origem WhatsApp chega ao passo de emissão ainda não-redigido. O que acontece?
Correto: a — PII é redigida antes da chamada de modelo (barreira 1) e re-checada antes de qualquer escrita (barreira 2). emitSafeSignal retorna undefined e o contador sinaliza. b contradiz a invariante ①: o gate é fail-closed, não best-effort. c descreve uma "redação na escrita" que não acontece para o sinal já cru — a barreira 2 valida e descarta, ela não conserta silenciosamente um sinal de canal privado que escapou da barreira 1. d erra o modo de falha: assertRedactedForEmit devolve um err tipado (nunca lança), então a run continua — fail-closed, não crash.
💬 Travou em algo? Eu sou seu professor neste curso — pergunte. "Por que o painel e o consenso, não só um?", "Como o offline torna T2/T3 grátis?", "O que é exatamente o score 6-dim de T0?". É só dizer.