A grande ideia
Barato e seguro primeiro
L0 = contracts (os tipos) + ETL T0 (o pipeline gratuito). Tudo que chega caro já passou por aqui.
1A 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.
Result<T,E> e ModelRunResult formam a cintura estreita que toda a stack respeita.assertRedactedForEmit e por que ele é fail-closed.tier, gate, residue e narrow waist são definidos aqui.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:
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.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.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.
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.
L0 de ponta a ponta — leitura da esquerda (corpus bruto) para a direita (tiers caros). A cintura estreita aperta tudo no meio.
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.
| Arquivo | O que define |
|---|---|
result.ts | Result<T,E> (ok/err) + tryCatch/tryCatchAsync — para operações falíveis que não chamam modelo. |
model.ts | ModelRunInput + ModelRunResult — o verdadeiro narrow waist: toda chamada de modelo passa por aqui. |
tier.ts | Enum T0…T4 + marcador LOCAL + helpers de ordem. |
registry.ts | MODEL_REGISTRY + pickCheapestForTier (visto a fundo na lição 2). |
wiki.ts · domain.ts | WikiPackage, BusinessSignal, Learning, OpportunityEdge. |
Por que o Alembic usa Result<T,E> em vez de simplesmente throw em erros de IO e parsing?
err(...), o chamador é obrigado pelo tipo a tratar a falha — nada escapa silenciosamente, e o mesmo estilo vale para chamadas de modelo (ModelRunResult).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)); } };
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:
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.
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.
{...}. Calculamos hash = sha256Hex(line) — a impressão digital do conteúdo.loadDedupeIndex. O hash já está lá (essa linha veio numa run anterior).outcome = 'duplicate'. Não validamos, não pontuamos, não roteamos. duplicates++ e seguimos.outcome e até onde o pipeline iria? (Resposta: 'invalid' — para logo após validateLlmWikiContract, sem score nem rota.)duplicate.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.
Cada eixo vem de uma evidência concreta no pacote. A barra de aprovação padrão é 18/30 (60%).
| Eixo | Vem de (evidência) | Nota cheia em |
|---|---|---|
completeness | chunkCount menos contentGaps | 12 chunks |
accuracy | researchRefs (refs resolvidas) | 6 refs |
clarity | qaPairs (pares de Q&A) | 8 pares |
actionability | understandingChars | 4000 chars |
novelty | understandingChars | 6000 chars |
provenance | hasRawSource resolvido? | 5 se sim, 1 se não |
meetsBar(score, 18) verifica o piso; total === soma dos seis eixos (garantido pelo schema).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.
hasRawSource = false e chunkCount = 0. Quanto vale provenance?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).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ília | Tier-alvo | Prioridade | Destino no T0 |
|---|---|---|---|
Transcripts | T2 | 0.9 | residue ↑ |
Discord · Circle | T2 | 0.8 | residue ↑ |
Skool | T2 | 0.75 | residue ↑ |
Whatsapp | T1 | 0.6 | residue ↑ |
Bookmarks · Skills | T1 | 0.5 | residue ↑ |
Repos | T1 | 0.4 | residue ↑ |
Unknown | T0 | 0.1 | fica 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.
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).
Sinal de canal privado sem redação não passa. Ele é dropado antes de chegar ao store de oportunidade.
PRIVATE_CHANNELS): whatsapp, discord, skool, circle. Sinais dessas famílias passam por redactSignal antes do assert.[REDACTED_EMAIL], [REDACTED_PHONE], [REDACTED_HANDLE], [REDACTED_TOKEN] — o que substitui o dado sensível no texto.emitSafeSignal em harness/funnel.ts.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.
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.)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).
Business/opportunity-graph.jsonl — sinais e arestas de oportunidade.Skills/learning/learnings.jsonl — aprendizados acumulados.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.ModelRunInput → ModelRunResult em ModelAdapter.run: a forma única por onde TODA chamada de modelo passa. Muitos chamadores acima, muitos provedores abaixo.$0 (e exclui Repos/Models e Repos/Prompts).meetsBar é 18/30 (60%).whatsapp, discord, skool, circle. Sem redação → err(private_unredacted), sinal dropado.A grande ideia
L0 = contracts (os tipos) + ETL T0 (o pipeline gratuito). Tudo que chega caro já passou por aqui.
1Contracts
Result para IO/parsing e ModelRunResult para modelos — uma forma que toda a stack respeita, e que deixa o sistema agnóstico de provider.
T0 pipeline
dedupe SHA-256 → validar contrato → score de 6 eixos → rotear por prior → emitir scored ou residue. Determinístico, stream, FsPort injetável.
3Roteamento
Transcripts→T2, Bookmarks/Repos→T1, Unknown fica em T0. O que aponta acima do piso vira residue para os tiers caros.
4Privacidade
assertRedactedForEmit: canal privado sem redação → err, sinal dropado. Stores append-only, endereçados por conteúdo.
runT0Pipeline e o laço. Depois score.ts e pii.ts. É a fonte de verdade desta lição.
# 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
Responda às três para fechar a lição.
Transcripts chega ao T0. O que acontece?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.assertRedactedForEmit devolve err em vez de lançar?Result/err), o chamador é obrigado a tratá-lo, e na dúvida o sinal não é escrito — privacidade fail-closed.ModelRunInput → ModelRunResult; tudo passa por ela.Result<T,E> torna o erro um valor — nunca throw em biblioteca.$0, read-only no source, append-only nos outputs.ModelAdapter.run e como o adapter offline, o gateway e os modelos locais entram pela mesma porta.