Parte II — Tronco e raízes

03 · O tronco: onde a água fica guardada

Este saber ainda não tem versão em SITEMAP.XML — mostrando em Português.

O baobá acumula até milhares de litros de água no tronco e atravessa meses de seca. O núcleo do Baobáxia guarda os saberes em arquivos simples, que sobrevivem a quedas de luz, discos trocados e anos sem rede.

O fruto da mucua: como uma galáxia mora no disco

Arquivos para ler, nesta ordem

  1. baobaxia/core/saber.py — o modelo (≈ 250 linhas).
  2. baobaxia/infra/storage.py — ler e gravar saber.bbx.
  3. baobaxia/core/repository.py — SaberRepository: uma galáxia.
  4. baobaxia/core/index.py — GalaxyIndex: o cache SQLite.
  5. baobaxia/core/relation_graph.py e core/registry.py.

O arquivo saber.bbx

---
uuid: 9f8e7d6c-1b2a-4c3d-8e9f-0a1b2c3d4e5f
nature: midia
slug: pescaria
name: Pescaria no rio
galaxia: 201b8213-…
mucua: 373cc7ef-…
creator: 4890f893-…
created: '2026-05-02T14:10:00'
visibility: public
applications: [acervo]
data:
  title: {pt: Pescaria no rio, en: Fishing on the river}
  tags: [pesca, rio]
  protecao: publico
content:
- {filename: foto.jpg, mimetype: image/jpeg, size: 182044,
   hash: SHA256E-s182044--3f2a….jpg}
---
Texto livre em markdown (vira data.body).

Legível por gente, versionável pelo git, e foto.jpg é um link simbólico do git-annex: o binário só existe nas mucuas que pediram uma cópia.

SaberRepository: uma galáxia

classDiagram
    class SaberRepository {
        +Path root
        +GalaxyIndex index
        +create(saber, author, commit) Saber
        +upsert_saber(saber) Saber
        +update(uuid, data, …) Saber
        +delete(uuid) bool
        +get(uuid) Saber
        +add_contents(uuid, files) Saber
        +reindex(incremental) int
        +reindex_saber(uuid) bool
        +refrescar_presenca() int
        -_saber_dir(saber) Path
        -_merge_content(dir, refs) list
        -_index_entry(saber) dict
        -_load(dir) Saber
        -_commit(msg, author)
    }
    class GalaxyIndex {
        +upsert(entry)
        +upsert_many(entries) int
        +aplicar_lote(entradas, remover, marcador)
        +get(uuid) dict
        +query(nature, tag, text, is_local, …) dict
        +assinaturas() dict
        +ultimo_commit_indexado() str
        -_criar_acesso()
    }
    class storage {
        <<module>>
        +read_bbx(path) (meta, body)
        +write_bbx(path, meta, body)
        +list_content_files(dir) list
    }
    class Sankofa {
        <<fachada>>
        +init(path)
        +add(path, root)
        +commit(root, msg)
        +sync(root, remote, data) bool
        +get / drop / whereis
    }
    SaberRepository *-- GalaxyIndex
    SaberRepository ..> storage
    SaberRepository ..> Sankofa

Gravar um saber (create)

sequenceDiagram
    participant S as SaberService
    participant R as SaberRepository
    participant St as storage
    participant G as Sankofa (git)
    participant I as GalaxyIndex
    S->>S: permissions.require(CREATE) e validate_data (schema da nature)
    S->>S: authorship.try_sign (assina com a chave do autor, se houver)
    S->>R: create(saber, author)
    R->>R: aplicações padrão da nature, slug, pasta
    R->>R: _merge_content (refs declarados + disco)
    R->>St: write_bbx(saber.bbx, to_metadata(), body)
    R->>G: add(saber.bbx) e commit("create midia: pescaria")
    R->>I: upsert(_index_entry(saber))
    S->>S: _pos_escrita: invalida o grafo e propaga às cópias em outras galáxias

O índice SQLite (core/index.py)

Uma tabela larga (saberes), tabelas auxiliares e uma tabela estreita acesso mantida por triggers — as listagens checam permissão de milhares de linhas sem ler os blobs JSON.

erDiagram
    saberes {
        TEXT uuid PK
        TEXT nature
        TEXT slug
        TEXT name
        TEXT galaxia
        TEXT mucua
        TEXT creator
        TEXT visibility
        TEXT created
        TEXT rel_path
        REAL lat
        REAL lon
        TEXT territorio
        TEXT busca "minúsculo e sem acento"
        TEXT meta_json
        TEXT data_json
        TEXT fs_sig "mtime:tamanho"
        INTEGER is_local
    }
    acesso {
        TEXT uuid PK
        TEXT protecao
        TEXT leitores
        TEXT editores
        TEXT status
        INTEGER ok "json válido"
    }
    tags { TEXT uuid TEXT tag }
    tipos { TEXT uuid TEXT tipo "imagem video audio pdf…" }
    applications { TEXT uuid TEXT application }
    fts { TEXT uuid TEXT text "FTS5" }
    meta { TEXT chave PK TEXT valor "ultimo_commit_indexado" }
    saberes ||--|| acesso : "trigger"
    saberes ||--o{ tags : ""
    saberes ||--o{ tipos : ""
    saberes ||--o{ applications : ""
    saberes ||--|| fts : ""

Detalhes que valem a leitura:

  • Leitores separados (_Leitores, _leitura("rapida"|"pesada")): uma consulta rápida (por uuid) nunca espera uma listagem pesada.
  • geracao(): um contador global de escritas; o cache de leitura (services/cache_leitura.py) usa esse número como chave — qualquer escrita invalida tudo de uma vez.
  • Busca sem acento: normalizar_busca (NFKD + minúsculas) dos dois lados. "acai" acha "açaí".

Reindex incremental: o tempo espiralar

Tempo espiralar: histórico e reindex

reindex(incremental=True) pergunta ao git o que mudou desde o ultimo_commit_indexado (_pastas_mudadas_no_git) e relê só essas pastas. Sem marcador, compara a assinatura de disco (fs_sig). O marcador é gravado na mesma transação dos dados: adiantado, ele faria saberes sumirem para sempre.

flowchart TD
    A[reindex incremental] --> B{marcador existe e é<br/>ancestral do HEAD?}
    B -- sim --> C[git diff marcador..HEAD<br/>→ pastas tocadas]
    C --> D[relê só essas pastas<br/>+ linhas com fs_sig vazio]
    B -- não --> E[varre saber.bbx do disco<br/>abre só os com fs_sig diferente]
    D --> F[aplicar_lote: entradas + remoções + marcador<br/>numa transação]
    E --> F
    F --> G[saber.bbx ilegível?<br/>mantém a versão anterior e avisa no log]

Presença de binário (git annex get/drop) não muda nenhum arquivo versionado — por isso existe refrescar_presenca(), que faz um listdir por pasta com anexo.

Práticas

  1. Em test_reindex_incremental.py, ache o teste que conta quantos saber.bbx foram relidos. Mude um saber e confira o número.
  2. Corrompa à mão o YAML de um saber.bbx (como um conflito de merge faria) e rode bbx reindex. O saber some da busca? Por quê não? (_load, reindex.)
  3. Abra o índice com sqlite3 .bbx/index.sqlite e rode SELECT nature, count(*) FROM acesso GROUP BY nature;.
  4. Leia _index_entry e explique de onde vem a coluna territorio de um saber que não declarou território.

Para conversar

  • O índice pode ser apagado a qualquer momento. Que liberdade isso dá a quem mantém o sistema? Que cuidado isso exige?

ver este capítulo como saber