Lição 19 · Fusão v3 · Conhecimento e ingestão · @alembic/ingestion ← índice
Alembic × Hermes — O Curso de Fusão v3 · Conhecimento e ingestão

@alembic/ingestion: a ponte dir-per-package

O acervo wiki do usuário vive em disco como um diretório por pacoteitems/<categoria>/<id>/ com sete arquivos + ponteiro raw — mas o ETL do funil só lê envelopes de UMA linha. A ponte é o walkFamily: enumera a família inteira, junta o master index e emite exatamente um package.jsonl por pacote, com header llm_wiki e uma evidence REAL destilada do próprio conteúdo. Read-only sobre o corpus, idempotente, e com a doutrina de exclusão INJETADA de cima.

1

A grande ideia: o corpus é sagrado; a ponte é uma linha

Todo pacote wiki real (Bookmarks / Transcripts / Repos sob ~/Documents/Resources) é um DIRETÓRIO de arquivos separados: source.md, understanding.md, research-index.md, qa.md, metadata.json, chunks.jsonl, media_manifest.json e um ponteiro raw — raw.pointer.json ou um diretório raw/ (packages/ingestion/src/family-package.ts:14–22, 30–46). O pipeline ETL, por sua vez, "only reads single-line package.jsonl envelopes" — então o leitor readWikiPackageDir é "the first half of the bridge": carrega o pacote em memória tipada para o adapter serializar em uma linha (family-package.ts:19–22). Todo o IO passa por um PackageReader injetado; os testes nunca tocam a árvore real de Resources (family-package.ts:24–26).

Pense como… uma biblioteca antiga ganhando fichário: cada estante-pasta (o pacote) continua intocada no lugar; o bibliotecário escreve UMA ficha catalográfica por pasta — resumo real do conteúdo, status, gaps — e é só a ficha que circula pelo resto do prédio. Onde quebra: ficha de papel desatualiza; aqui a ficha é derivada dos arquivos e reescrever é proibido (append-only, no-op se já existe).

launch-academy · 03-technical-architecture-deep-dive · s0
2

Em uma imagem: do pacote em disco à bridge line

items/<cat>/<id>/ source.md · understanding.md research-index.md · qa.md metadata.json · chunks.jsonl media_manifest.json raw.pointer.json | raw/ readWikiPackageDir PackageReader injetado + master record da família (canonical_package_path) emitFamilyBridge header llm_wiki source_layer=família · wiki_layer content_status + gaps (blocked ok) evidence REAL (T1 come isto) top-3 chunks + understanding[0:600] + summary/tags do master · cap 2000 vazia ⇒ family.empty_evidence (err) valida wikiBridgeLineSchema ANTES de escrever 1 byte package.jsonl (1 linha) escrita atômica: tmp → rename já existe? NO-OP (written:false) excluído? pulado (excluded:true) blocked? emite MESMO ASSIM, com o gap no header (1ª classe)
family-adapter.ts: evidência é texto destilado de verdade (nunca stub) porque o T1 do funil a consome direto; a linha só nasce validada (wikiBridgeLineSchema, :244–253), a escrita é atômica tmp→rename (:67–83) e um package.jsonl existente nunca é sobrescrito (:258–261).

A evidence é o sinal T1 load-bearing — "the funnel feeds it straight to the local extraction model" — então ela é montada do conteúdo real: os corpos dos 3 primeiros chunks (EVIDENCE_TOP_CHUNKS), os primeiros 600 caracteres do understanding.md (EVIDENCE_UNDERSTANDING_CHARS) e o summary+tags do master record, tudo achatado de markdown e limitado a 2000 caracteres (EVIDENCE_MAX_CHARS) (family-adapter.ts:33–37, 52–57, 150–175). Pacote sem texto extraível nenhum não vira linha vazia: vira o erro permanente family.empty_evidence (family-adapter.ts:231–239).

3

walkFamily: fan-out com dois formatos de path

O emitFamilyBridge cuida de UM pacote; walkFamily materializa a família inteira: enumera items/<categoria>/<id>/ (categorias e ids em ordem sorted, entradas ocultas tipo .DS_Store puladas — family-walk.ts:203–219), carrega index/<família>.master.jsonl num Map por canonical_package_path (linha ruim pulada; última linha do mesmo path vence — family-walk.ts:141–156) e chama o adapter uma vez por diretório. A sutileza central são os dois formatos de path mantidos distintos de propósito (family-walk.ts:37–42, 258–263):

formaexemplopara quê
master-join keyitems/design/195…173lookup no master index (o path é RELATIVO à família no disco, sem prefixo)
corpus-relativeBookmarks/items/design/195…173o isExcluded injetado e o header — o 1º segmento É a família
familyRoot items/ + index/ (ex.: …/Bookmarks) enumeratePackages (sorted) .oculto pulado · falha de categoria coletada, caminhada segue master index → Map ausente? segue com Map vazio (defaults do adapter) emitFamilyBridge 1× por pacote summary written skipped excluded blocked errors[] 2ª caminhada: written = 0 COLLECT, NEVER ABORT: erro por pacote entra em errors[] e o walk nunca lança (family-walk.ts:48–50)
walkFamily = enumeração + join + adapter por pacote, com contadores. Idempotente por construção: a emissão no-opa sobre package.jsonl existente.

As três doutrinas do walker estão gravadas no header do arquivo (family-walk.ts:44–53): CONTRACTS-ONLY — este pacote nunca importa @alembic/etl; o predicado isExcluded é INJETADO pela camada de cima (a CLI injeta o canônico do etl em commands.ts:620–621), então a camada de baixo não carrega doutrina de destilação; COLLECT, NEVER ABORT — falha por pacote vira item de errors[] e a caminhada continua; APPEND-ONLY / IDEMPOTENT — a segunda caminhada escreve 0. O que fica excluído pelo predicado do etl: subárvores como Repos/Models e Repos/Prompts (family-adapter.ts:44–47).

4

O caso YouTube — e o gotcha do primeiro segmento

A família Transcripts tem uma particularidade de layout REAL: o master index não fica em Transcripts/index/, e sim em Transcripts/YouTube/index/transcripts.master.jsonl — confirmado no próprio doc do walker (family-walk.ts:31–35). Isso funciona por design: walkFamily aceita QUALQUER diretório que contenha items/ + index/ como familyRoot; a identidade da família (identity.family) e o stem do índice (indexFamily) são parâmetros separados do path. Na CLI, resolveWikiFamily tenta classificar o basename e depois o path completo pelo 1º segmento; quando nada resolve (ex.: basename YouTube), --family Transcripts pina a identidade e o root aninhado caminha normalmente (commands.ts:560–580).

Gotcha que custou caro (family-prefix)classifyFamily lê o PRIMEIRO segmento do path relativo ao corpus (packages/etl/src/priors.ts:70–76). Consequência no distill: apontar alembic distill para a RAIZ DA FAMÍLIA (ex.: …/Resources/Bookmarks) faz cada path relativo começar em items/… ⇒ família Unknown ⇒ residue 0, silencioso. Aponte para o PAI (…/Resources) — foi esse detalhe que separou "rota learnings nunca produziu" de 1.022 learnings em 06-07. Escopo de uma família só = o truque cpio com os package.jsonl.

A cadeia típica completa, cada elo um comando inspecionável: alembic wiki-bridge <família> (escreve as bridge lines) → alembic distill <pai-de-Resources> --offline (roteia as linhas prefixadas pela família para residue/T1) → alembic embed-index <família> (índice offline de embeddings) → alembic search "<consulta>" <família> (recuperar os chunks mais próximos — a próxima lição).

alembic-completo · 0006-hot-paths · s0
1
package.jsonl por pacote — nunca sobrescrito
2.589
signals no grafo — o funil que a ponte alimenta
1.022
learnings únicos — nascidos do corpus bridgeado
5

Cheque seu modelo mental

Por que walkFamily mantém DOIS formatos de path para o mesmo pacote (com e sem o prefixo da família)?
Correto: no disco, canonical_package_path é family-relativo (items/<cat>/<id>) — essa é a MASTER-JOIN KEY; mas o predicado de exclusão do etl e a classificação de família leem o path CORPUS-relativo, cujo 1º segmento é a família (family-walk.ts:37–42, 258–263). Misturar os dois é exatamente o que gera o gotcha do distill apontado na raiz errada.
Um pacote tem os sete arquivos, mas nenhum texto aproveitável (chunks vazios, understanding vazio, sem master). O que a ponte emite?
Correto: evidência vazia é erro permanente, não linha degradada (family-adapter.ts:231–239). A evidence é o payload que o T1 come direto — emitir stub vazio geraria chamada paga sobre nada. O walker coleta o erro em errors[] e segue para o próximo pacote (collect, never abort).
Camada técnica — comandos copy-paste

Bridge e caminhada ($0; --dry-run nem escreve):

# prova sem escrita: conta o que SERIA escrito
alembic wiki-bridge ~/Documents/Resources/Bookmarks --dry-run --json

# a caminhada real (idempotente: rode 2× e veja written cair a 0)
alembic wiki-bridge ~/Documents/Resources/Bookmarks

# o caso YouTube: root aninhado + família pinada
alembic wiki-bridge ~/Documents/Resources/Transcripts/YouTube --family Transcripts --dry-run

# distill: SEMPRE no PAI de Resources (gotcha do 1º segmento)
alembic distill ~/Documents/Resources --offline --json

# as doutrinas e os dois formatos de path, na fonte
sed -n '37,53p' packages/ingestion/src/family-walk.ts
sed -n '52,57p' packages/ingestion/src/family-adapter.ts
sed -n '70,76p' packages/etl/src/priors.ts

Nota: walkCorpus/o walker NÃO seguem symlinks — pacote symlinkado não entra na caminhada.

O que levar desta lição
Pergunta de acompanhamento sugerida: "se a evidence é capada em 2000 caracteres, o que o funil perde — e onde o resto do pacote volta a ser lido?" — Na próxima lição, a resposta parcial: @alembic/embeddings indexa os chunks INTEIROS e o alembic search os recupera por cosseno — o read-path RAG.