Ao fim desta lição você sabe como o pipeline T0 processa 100% do corpus a US$ 0, por que re-rodar não duplica nada — e por que apontar o distill para a pasta errada produz residue 0 em silêncio (a gotcha que custou caro).
packages/etl/src/pipeline.ts — o T0 inteiro em um arquivo
pipeline.ts (o walk + dedupe + rota), priors.ts (a família pelo 1º segmento) e stores.ts (os livros-razão append-only) são as três peças que esta lição destila.
FsPort.opportunity-graph.jsonl e learnings.jsonl.Antes de qualquer modelo ser pago, o corpus inteiro passa por um funil grátis e determinístico: o pipeline T0. Ele caminha o diretório como stream (nunca carrega a árvore na memória), lê cada .jsonl linha a linha, calcula o SHA-256 de cada linha e consulta um livro-razão: “já vi este conteúdo?”. Duplicata? Pula. Novo? Valida contra o contrato wiki, pontua com o scorer determinístico — e o que merece um tier mais alto vira residue, a fila de trabalho dos tiers pagos (lição 05).
Duas propriedades pagam o aluguel: re-rodar é de graça (corpus inalterado ⇒ só outcomes duplicate, zero deltas — pipeline.ts:260-263) e nada é destruído (fonte é read-only; saídas são append-only). É o chão de fábrica sobre o qual as DUAS cadeias de valor da ADR-0002 operam.
Pense como… a esteira de raio-X do aeroporto: toda mala passa, nenhuma é aberta à força (read-only), as suspeitas ganham etiqueta para inspeção manual (residue) e a câmera grava tudo num log que só cresce (ledger). Onde quebra: no aeroporto a fila anda uma vez; aqui a MESMA mala pode voltar amanhã — e a esteira a reconhece pelo hash e nem gasta energia de novo.
runT0Pipeline (pipeline.ts:265-298) orquestra: walkCorpus (114-140) emite arquivos .jsonl e marcadores de exclusão — Repos/Models e Repos/Prompts nunca são descidos (priors.ts:34-37, pesos de modelo e corpora de prompt não são sinal destilável). Cada linha passa por processLine (177-248): sha256Hex(raw) → índice de dedupe → JSON.parse → validateLlmWikiContract → recordProcessed no ledger → rota. Os cinco destinos possíveis são o tipo ItemOutcome: scored | residue | blocked | duplicate | invalid (pipeline.ts:44-49).
Gotcha herdada do walk: readDir devolve isDirectory/isFile do dirent real (fs-port.ts:138-149) — um symlink não é nem um nem outro, então walkCorpus não segue symlinks. Família symlinkada = família invisível.
A ETL nunca importa node:fs diretamente nas funções de negócio: todas recebem um FsPort — a porta de efeito colateral. Isso compra duas coisas: testabilidade (um fake em memória roda a suíte hermética) e auditabilidade (a superfície de efeito é uma interface de 8 métodos, não chamadas espalhadas).
| Método (fs-port.ts:61-83) | Papel | Invariante |
|---|---|---|
stat / readDir / openLineStream | ler o corpus | read-only; ENOENT não lança (61-65, 121-123) |
appendLine | escrever ledger/residue | append-only — só adiciona linha (72-73) |
ensureDir | criar o output dir | uma vez, recursivo |
writeFileAtomic | trocar um arquivo inteiro | temp + rename: ninguém vê arquivo pela metade (74-80, 172-176) |
readText / joinPath | utilitários | arquivos pequenos; separador da plataforma |
FsPort (@alembic/etl) for testable IO; avoid direct fs in packages other than ETL/CLI.” A porta nasceu aqui e virou o padrão de IO de todo o motor — inclusive dos subsistemas portados do Hermes (lição 06 em diante).Antes de gastar um centavo, cada arquivo ganha um prior: uma aposta barata de “quão promissor é isso?” baseada apenas na família da fonte. E a família vem de UM lugar: o primeiro segmento do caminho relativo ao corpus (classifyFamily, priors.ts:70-76). Transcripts/… mira T2 com prioridade 0.9; Bookmarks/… mira T1; segmento desconhecido vira Unknown → T0 (priors.ts:54-64).
E aqui mora a pegadinha mais cara do motor: routesToResidue só emite residue quando o tier do prior é estritamente maior que o piso T0 (priors.ts:99-102). Unknown mira T0 ⇒ nunca vira residue ⇒ os tiers pagos nunca veem o item. Sem erro, sem aviso: residue 0, silencioso.
alembic distill ~/Documents/Resources/Bookmarks --offline (a raiz da própria família). O que acontece?items/…, o 1º segmento não casa com nenhuma família (priors.ts:70-76) ⇒ Unknown com prior T0 ⇒ routesToResidue exige tier estritamente > T0 (priors.ts:99-102) ⇒ nada sobe. O comando termina “com sucesso”. Aponte sempre no PAI de Resources.O que o funil produz precisa durar. A ETL define dois stores content-addressed sob o data dir — e as quatro garantias deles (stores.ts:26-34): append-only (linha nunca é reescrita), content-addressed (cada registro carrega o SHA-256 do seu JSON canônico; conteúdo idêntico = no-op), atômico (via writeFileAtomic) e validado (Zod antes de persistir — registro malformado é impossível de gravar).
Business/opportunity-graph.jsonl (stores.ts:40) — edges tipados + sinais minerados. Hoje: 2.589 signals.
Skills/learning/learnings.jsonl (stores.ts:43) — os learnings da ADR-0002. Primeira safra real: 1.022.
canonicalJson — chaves ordenadas recursivamente (stores.ts:83-93) — então dois registros estruturalmente iguais colapsam no mesmo hash mesmo que a ordem das chaves difira. E o hash do envelope é recomputado na leitura, nunca confiado às cegas (stores.ts:70-77).A função que decide TUDO sobre famílias tem sete linhas. Leia com atenção o split — é ele que a gotcha explora:
export const classifyFamily = (relativePath: string): SourceFamily => { const head = relativePath.split(/[\\/]/).find((seg) => seg.length > 0) ?? ''; const match = SOURCE_FAMILIES.find( (family) => family !== 'Unknown' && family === head, ); return match ?? 'Unknown'; }; /** The SignalPrior for a corpus-relative path. Pure. */ export const priorFor = (relativePath: string): SignalPrior => FAMILY_PRIORS[classifyFamily(relativePath)];
As nove famílias reconhecidas estão em priors.ts:15-25: Bookmarks, Transcripts, Repos, Circle, Discord, Skool, Whatsapp, Skills — e Unknown como fallback. Os priors por família (tier + prioridade 0–1) em priors.ts:54-64. Compare com o consumo em pipeline.ts:282 (const prior = priorFor(entry.relativePath)): o caminho é SEMPRE relativo ao corpusDir que você passou na CLI — por isso o alvo importa tanto.
# o run T0 hermético, $0 (aponte no PAI de Resources!) alembic distill ~/Documents/Resources --offline # prova do dedupe: rode duas vezes e compare — a 2ª só conta duplicates alembic distill ~/Documents/Resources --offline # de novo # inspecione os artefatos append-only do T0 (no data dir do run) wc -l _alembic-processed.jsonl _alembic-residue.jsonl head -1 Business/opportunity-graph.jsonl | head -c 300 # a cadeia típica de uma família wiki alembic wiki-bridge ~/Documents/Resources/Bookmarks --dry-run alembic distill ~/Documents/Resources --offline
Fixe os quatro reflexos do operador de ETL.
sha256Hex, dedupe.ts:38) e conferida no ledger _alembic-processed.jsonl — hash visto ⇒ outcome duplicate, zero escrita (dedupe.ts:90-112).appendStoreRecord valida com o schema Zod ANTES de escrever (stores.ts:148-166) — parse falhou, nada persiste. E o hash canônico deduplica conteúdo idêntico.ModelAdapter, a cintura estreita.