Ao fim desta lição você sabe orquestrar runFunnel de ponta a ponta, explicar por que a ADR-0002 dá a signals e learnings orçamentos, stores e relatórios separados — e contar a história do primeiro run real: 1.022 learnings de Bookmarks.
packages/harness/src/funnel.ts — o orquestrador L0→L3 inteiro
funnel.ts (runFunnel, collectResidue, as duas cadeias) + etl/budget.ts (BudgetGuard, RouteBudgets) — esta lição destila os dois. Os invariantes de segurança estão gravados no topo do arquivo (funnel.ts:72-84).
--budget · --learnings-budget) e o relatório routes.collectResidue + bridgeEvidenceOf e dizer o que o T1 realmente cita.O harness é onde as três lições anteriores viram UMA máquina. runFunnel pega um corpus e o conduz por quatro tiers: T0 — o substrato determinístico da lição 03 (100% do corpus, $0); T1 — um modelo LOCAL extrai um BusinessSignal de cada item do residue; T2 — uma shortlist FRONTIER refina os sinais fortes, em lotes, cada chamada paga pré-aprovada pelo BudgetGuard; T3 — um council de três membros debate e um verifier maker-checker decide: só um GO verificado emite edges + learnings.
E há três invariantes que o arquivo declara “load-bearing, do not regress” (funnel.ts:72-84): PII — sinal de canal privado é redigido e passa por assertRedactedForEmit ANTES de qualquer escrita (não passou ⇒ descartado, nunca emitido); BUDGET — toda chamada paga é embrulhada num check fail-closed (projeção estoura o cap ⇒ a chamada NÃO acontece e o tier degrada); APPEND-ONLY — resultados fluem para os stores por escrita validada e content-addressed; a fonte permanece read-only.
Pense como… uma refinaria com dois dutos de saída: o mesmo petróleo bruto (residue) alimenta o duto de combustível (signals → oportunidades de negócio) e o duto de lubrificante (learnings → melhoria do próprio motor). Cada duto tem seu PRÓPRIO medidor de vazão e sua PRÓPRIA válvula de corte — entupir um jamais fecha o outro. Onde quebra: numa refinaria os dutos competem pelo mesmo cru; aqui um item pode ser processado PELAS DUAS rotas (funnel.ts:807-809).
A ordem de orquestração exata (doc em funnel.ts:864-871; corpo em 873-943): runT0Pipeline → collectResidue → runT1Extraction (cadeia de sinais, medida SÓ no cap de signals) → gate de PII (emitSafeSignal) → runT2Shortlist (força mínima 3, lotes de 8 — defaults em 698-699) → emitSignals → runT3Council → emitResults → e, SÓ se o budget de learnings existe, runLearningsRoute + emitLearningsRoute sobre o MESMO residue.
O T3 usa um council sintético fixo de três papéis — optimist, analyst, pessimist (funnel.ts:585-589) — e a emissão exige DUPLA trava: isPanelEmissionApproved(report) E consensus.decision === 'GO' (563-568). Membro bloqueado por budget é CONTADO (não silenciado) para o relatório por rota nunca sub-reportar (598-627).
A ADR-0002 diz: “o budget vira dois knobs, um por cadeia de valor”. Em código, o campo budget de FunnelOptions aceita TRÊS formas (funnel.ts:167-174): um número ou um BudgetGuard (a forma LEGADA — funda só a cadeia de sinais, run byte-idêntico ao de sempre) ou um mapa RouteBudgets {signals, learnings} — a forma de dois knobs que ATIVA a cadeia de learnings com cap independente (resolveRouteBudgets, budget.ts:216-221).
O contrato de honestidade do relatório: report.routes só EXISTE numa run de dois knobs (funnel.ts:150-160 + 919-922) — o JSON de uma run legada permanece byte-idêntico para consumidores. E routes.signals.budgetBlocked soma TODOS os bloqueios fail-closed da cadeia: T1 + T2 + membros do council T3 (funnel.ts:930-931).
| Dimensão | Cadeia SIGNALS | Cadeia LEARNINGS |
|---|---|---|
| Knob CLI | --budget (ou ALEMBIC_BUDGET_USD) | --learnings-budget (ou ALEMBIC_LEARNINGS_BUDGET_USD) |
| Store | Business/opportunity-graph.jsonl | Skills/learning/learnings.jsonl |
| Tiers pagos | T1 extração + T2 shortlist + T3 council | extração única em T2 (LEARNINGS_ROUTE_TIER, funnel.ts:756) |
| Se o cap esgota | bloqueia SÓ esta rota (contado no report) | bloqueia SÓ esta rota (contado no report) |
| Sem o knob | sempre roda (cadeia primária) | rota INATIVA — nenhum store tocado, run byte-idêntico |
funnel.ts:773-776). E offline o adapter enlatado devolve texto com formato de SINAL — que não parseia como learning ⇒ ZERO learnings, $0, nada fabricado (funnel.ts:810-812). A rota só produz quando um modelo real encontra lição explícita no texto.--learnings-budget esgota no meio. O que acontece com a cadeia de signals?funnel.ts:96-99; budget.ts:155-159). Os medidores são objetos separados (ResolvedRouteBudgets), cada um com seu check/record — o bloqueio de um é contado no routes.learnings.budgetBlocked e a cadeia de signals nem fica sabendo.Depois do T0, o funil percorre o corpus de novo e monta a fila dos tiers pagos: collectResidue (funnel.ts:316-341) pega cada arquivo cuja família mira tier > T0, valida cada linha contra o contrato wiki (só valid passa) e empacota um ResidueItem: hash, caminho, família, canal (para o gate de PII) e — o campo que alimenta o modelo — a evidência.
É aqui que entra a função pequena e importante: bridgeEvidenceOf (funnel.ts:308-314). Uma linha de bridge (o wiki-bridge da lição 03) carrega um campo evidence com o texto destilado REAL que o adapter de família escreveu. A função prefere esse campo ao registro serializado inteiro — para o sinal do T1 citar conteúdo, e não um bundle de paths. Linha legada sem evidence? Cai no raw, sem quebrar.
funnel.ts:219-236). E na emissão, emitSafeSignal rediga + afere com assertRedactedForEmit; falhou ⇒ o sinal é descartado, fail-closed (funnel.ts:281-295).O guarda é um medidor por run, sem IO: capUsd fixo, spentUsd() acumulado, e o protocolo em dois tempos — check(estimativa) ANTES de cada chamada paga, record(custo real) DEPOIS. Se gasto + projeção ultrapassa o cap, volta um BudgetExceeded tipado ({reason, spentUsd, projectedUsd, capUsd, message}) e a chamada não acontece. Ele “never silently proceeds and never throws” (budget.ts:29-40).
A projeção usa uma estimativa grosseira e barata: ~4 caracteres por token de entrada + 256 de saída (estimateUsage, funnel.ts:467-470), precificada pela tarifa do TIER no registry — mesmo quando o modelo de fio foi sobrescrito pelo catálogo, a cobrança usa o pricingModelId do tier, nunca fica sem medição (funnel.ts:343-348 + 409-426).
Bloqueou ⇒ o tier DEGRADA em vez de estourar: no T2 o sinal segue sem refino (funnel.ts:472-476); no T3 o membro fica sem voto e o quorum decide (598-604); no T1/learnings o item é pulado e CONTADO. Nunca uma exceção, nunca um gasto surpresa.
Cap ≤ 0 = “só tier grátis”: qualquer chamada precificada é bloqueada (budget.ts:117-120). T0 é sempre $0 e nunca bloqueia (38-40, 99-100). Modelo fora do registry projeta $0 — e o gasto real ainda é medido pelo tier no meterSpend.
Em 2026-07-06 a cadeia de learnings produziu pela primeira vez na história do motor. Não por um recurso novo — o código já estava pronto — mas porque três pedras saíram do caminho, todas ensinadas neste módulo:
package.jsonl num pai temporário (truque cpio).deepseek-v4-pro — AccessDenied na conta do gateway. Declarar gemini-3.5-flash antes fez o MESMO empate resolver para um modelo utilizável (a lição 02 inteira num caractere de ordem).--learnings-budget ativa a rota; o extractor T2 lê o MESMO residue com o prompt anti-fabricação e o appendLearning content-addressed grava sem duplicar.~/.alembic-learnings-bkm para auditoria — separada do data dir de produção.A função que faz o T1 citar conteúdo real (e não paths) — pequena o bastante para caber inteira:
export const bridgeEvidenceOf = (parsed: unknown, raw: string): string => { if (parsed !== null && typeof parsed === 'object' && 'evidence' in parsed) { const ev = (parsed as { readonly evidence?: unknown }).evidence; if (typeof ev === 'string' && ev.trim().length > 0) return ev; } return raw; };
Abra packages/harness/src/funnel.ts e leia na sequência: os invariantes (59-84), collectResidue (316-341), a cadeia de sinais (runT1Extraction 435-464, runT2Shortlist 477-504, runT3Council 543-582) e a rota learnings (756-862). Depois o medidor em packages/etl/src/budget.ts:120-152. A suíte que prova tudo isso offline está em funnel.test.ts e budget.test.ts.
# run hermético $0 (nenhuma cadeia paga; T0 + relatório) alembic distill ~/Documents/Resources --offline # a run de DOIS knobs (ADR-0002): caps independentes por rota alembic distill ~/Documents/Resources --budget 5 --learnings-budget 3 # o relatório por rota SÓ existe na run de dois knobs alembic distill ~/Documents/Resources --offline --json | head -c 400 # sem chave "routes" # os dois stores, lado a lado (sob o data dir) wc -l Business/opportunity-graph.jsonl Skills/learning/learnings.jsonl # a safra isolada do 1º run real ls ~/.alembic-learnings-bkm
Feche o módulo 1 com as quatro cartas do operador de funil.