Lição 05 · Fundamentos · Alembic × Hermes v3 · @alembic/harness
Alembic × Hermes — O Curso de Fusão v3 · Visual Course

@alembic/harness — o funil T0→T3 e as duas cadeias

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.

Leia primeiro (fonte primária)
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).

Você vai conseguir
  • Narrar a ordem exata do funil: T0 → residue → T1 → PII → T2 → T3 → emissão → rota learnings.
  • Explicar os dois knobs (--budget · --learnings-budget) e o relatório routes.
  • Ler collectResidue + bridgeEvidenceOf e dizer o que o T1 realmente cita.
  • Provar que o BudgetGuard bloqueia ANTES de gastar — e degrada em vez de estourar.
Leia a versão simples, ou abra a camada técnica em qualquer seção.
1

A grande ideia


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).

Por baixo do capô

A ordem de orquestração exata (doc em funnel.ts:864-871; corpo em 873-943): runT0PipelinecollectResiduerunT1Extraction (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) → emitSignalsrunT3CouncilemitResults → 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).

2

Em uma imagem


fusao · 15-the-funnel · s0
T0 runT0Pipeline 100% corpus · $0 collectResidue evidence = bridgeEvidenceOf CADEIA SIGNALS (cap próprio: --budget) T1 local extrai sinal + PII T2 frontier força ≥3 · lotes 8 T3 council 3 votos + verifier Business/opportunity-graph.jsonl GO verificado ⇒ edges + sinais PII-safe CADEIA LEARNINGS (cap próprio: --learnings-budget · opt-in) runLearningsRoute (T2) MESMO residue · PII pré-call Skills/learning/learnings.jsonl só o que o texto EXPLICITAMENTE apoia Esgotar o cap de UMA rota bloqueia SÓ aquela rota — a outra segue intocada (funnel.ts:95-99).
runFunnel (funnel.ts:873-943): T0 compartilhado, bifurcação após o residue, e cada cadeia com seu medidor e seu store. A rota learnings só existe quando o 2º knob é dado.
3

As duas cadeias — ADR-0002 em código


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).

medidor SIGNALS (--budget) check() → record() · T1 + T2 + T3 council bloqueio conta em routes.signals.budgetBlocked store: Business/opportunity-graph.jsonl medidor LEARNINGS (--learnings-budget) check() → record() · extração única em T2 bloqueio conta em routes.learnings.budgetBlocked store: Skills/learning/learnings.jsonl isolados report.routes {signals, learnings} SÓ existe na run de dois knobs — a run legada omite a chave (funnel.ts:919-922)
ADR-0002 em uma imagem: dois BudgetGuards separados (budget.ts:216-221), cada um com cap, spend, bloqueios e store PRÓPRIOS. Um esgotar nunca drena o outro.
DimensãoCadeia SIGNALSCadeia LEARNINGS
Knob CLI--budget (ou ALEMBIC_BUDGET_USD)--learnings-budget (ou ALEMBIC_LEARNINGS_BUDGET_USD)
StoreBusiness/opportunity-graph.jsonlSkills/learning/learnings.jsonl
Tiers pagosT1 extração + T2 shortlist + T3 councilextração única em T2 (LEARNINGS_ROUTE_TIER, funnel.ts:756)
Se o cap esgotabloqueia SÓ esta rota (contado no report)bloqueia SÓ esta rota (contado no report)
Sem o knobsempre roda (cadeia primária)rota INATIVA — nenhum store tocado, run byte-idêntico
Anti-fabricação na rota learnings
O prompt da extração exige: “Only emit a learning EXPLICITLY supported by the text; otherwise emit {}” (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.
Numa run de dois knobs, o --learnings-budget esgota no meio. O que acontece com a cadeia de signals?
Isolamento por rota é O ponto da ADR-0002: “exhausting one route's cap never starves the other” (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.
4

collectResidue & bridgeEvidenceOf


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.

PII antes do modelo, não depois: o gate roda DUAS vezes na cadeia. Na montagem do input do T1, evidência de canal privado é redigida ANTES da chamada — PII crua jamais alcança um adapter possivelmente conectado à rede (funnel.ts:219-236). E na emissão, emitSafeSignal rediga + afere com assertRedactedForEmit; falhou ⇒ o sinal é descartado, fail-closed (funnel.ts:281-295).
5

O BudgetGuard fail-closed


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).

fusao · 27-tiers-cost-and-budget · s1
o que “fail-closed” significa aqui

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.

as bordas honestas

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.

6

O run dos 1.022 — a rota learnings produziu


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:

Exemplo resolvido — como o primeiro run real nasceu
1
Mirar o corpus no PAI de Resources. Apontado na raiz da família, o 1º segmento relativo não é família ⇒ Unknown ⇒ residue 0, silencioso (a gotcha da lição 03). Escopo de UMA família: recriar só os package.jsonl num pai temporário (truque cpio).
2
Destravar o leilão T2 (PR #157). O empate de 0.0005 resolvia para 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).
3
Rodar com o segundo knob. --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.
4
Colher e isolar. A safra ficou isolada em ~/.alembic-learnings-bkm para auditoria — separada do data dir de produção.
1.022
learnings únicos (Bookmarks, 1º run real)
100%
com sourceRefs + hash (proveniência total)
0,94
confidence média (μ) da safra
2
knobs de budget — e nenhum roubou do outro
Releia o arco: a lição 02 deu o leilão e o empate; a 03 deu o T0, a família e a gotcha; a 04 deu a cintura que chama o modelo; esta lição amarrou tudo — e o resultado é um número no store, não uma promessa.
7

No código


A função que faz o T1 citar conteúdo real (e não paths) — pequena o bastante para caber inteira:

packages/harness/src/funnel.ts:308-314 (trecho real, não editado)
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;
};

Acesse você mesmo

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.

Camada técnica — comandos desta seção
# 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
8

Experimente


Feche o módulo 1 com as quatro cartas do operador de funil.

ordem
Recite a ordem do runFunnel.
clique para virar
T0 → collectResidue → T1 (local) → gate PII → T2 (shortlist) → T3 (council+verifier) → emissão append-only → rota learnings opt-in (funnel.ts:864-871).
routes
Quando o report tem a chave “routes”?
clique para virar
SÓ na run de dois knobs (RouteBudgets dado). Run legada omite a chave inteira — o JSON fica byte-idêntico para consumidores (funnel.ts:150-160, 919-922).
budget
O que budgetBlocked de signals soma?
clique para virar
TODOS os bloqueios fail-closed da cadeia: T1 + T2 + membros do council T3 (funnel.ts:930-931). Membro bloqueado é contado, nunca silenciado (598-627).
história
As 3 pedras que seguraram os 1.022?
clique para virar
① corpus na pasta errada (family-prefix ⇒ residue 0); ② empate T2 resolvendo para um modelo AccessDenied (PR #157 trocou a ordem); ③ a rota exigia o 2º knob (--learnings-budget).
Pergunta de fechamento do módulo: “dos três invariantes do funil (PII, budget, append-only), qual EU quebraria primeiro por descuido — e que teste me pegaria?” Teaser da lição 06: entramos na herança Hermes de verdade — a memória multi-store (episodic/semantic/procedural/decision/transcript) e o SkillStore de 148 skills que os AI Employees carregam no prompt.