Parte II — Tronco y raíces

03 · El tronco: donde se guarda el agua

El baobab acumula hasta miles de litros de agua en el tronco y atraviesa meses de sequía. El núcleo de Baobáxia guarda los saberes en archivos simples, que sobreviven a cortes de luz, discos cambiados y años sin red.

El fruto de la mucua: cómo vive una galaxia en el disco

Archivos para leer, en este orden

  1. baobaxia/core/saber.py — el modelo (≈ 250 líneas).
  2. baobaxia/infra/storage.py — leer y escribir saber.bbx.
  3. baobaxia/core/repository.py — SaberRepository: una galaxia.
  4. baobaxia/core/index.py — GalaxyIndex: la caché SQLite.
  5. baobaxia/core/relation_graph.py y core/registry.py.

El archivo 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 libre en markdown (se vuelve data.body).

Legible por personas, versionable por git, y foto.jpg es un enlace simbólico de git-annex: el binario solo existe en las mucuas que pidieron una copia.

SaberRepository: una galaxia

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

Grabar un 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) y validate_data (schema de la nature)
    S->>S: authorship.try_sign (firma con la clave del autor, si la hay)
    S->>R: create(saber, author)
    R->>R: aplicaciones por defecto de la nature, slug, carpeta
    R->>R: _merge_content (refs declarados + disco)
    R->>St: write_bbx(saber.bbx, to_metadata(), body)
    R->>G: add(saber.bbx) y commit("create midia: pescaria")
    R->>I: upsert(_index_entry(saber))
    S->>S: _pos_escrita: invalida el grafo y propaga a las copias en otras galaxias

El índice SQLite (core/index.py)

Una tabla ancha (saberes), tablas auxiliares y una tabla estrecha acesso mantenida por triggers — los listados comprueban permisos de miles de filas sin leer los 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úsculas y sin acentos"
        TEXT meta_json
        TEXT data_json
        TEXT fs_sig "mtime:tamaño"
        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 : ""

Detalles que vale la pena leer:

  • Lectores separados (_Leitores, _leitura("rapida"|"pesada")): una consulta rápida (por uuid) nunca espera a un listado pesado.
  • geracao(): un contador global de escrituras; la caché de lectura (services/cache_leitura.py) usa ese número como clave — cualquier escritura invalida todo de una vez.
  • Búsqueda sin acentos: normalizar_busca (NFKD + minúsculas) de los dos lados. "acai" encuentra "açaí".

Reindex incremental: el tiempo espiral

Tiempo espiral: historial y reindex

reindex(incremental=True) le pregunta a git qué cambió desde el ultimo_commit_indexado (_pastas_mudadas_no_git) y relee solo esas carpetas. Sin marcador, compara la firma de disco (fs_sig). El marcador se graba en la misma transacción que los datos: adelantado, haría que algunos saberes desaparecieran para siempre.

flowchart TD
    A[reindex incremental] --> B{¿el marcador existe y es<br/>ancestro del HEAD?}
    B -- sí --> C[git diff marcador..HEAD<br/>→ carpetas tocadas]
    C --> D[relee solo esas carpetas<br/>+ filas con fs_sig vacío]
    B -- no --> E[recorre los saber.bbx del disco<br/>abre solo los de fs_sig distinto]
    D --> F[aplicar_lote: entradas + borrados + marcador<br/>en una transacción]
    E --> F
    F --> G[¿saber.bbx ilegible?<br/>mantiene la versión anterior y avisa en el log]

La presencia de un binario (git annex get/drop) no cambia ningún archivo versionado — por eso existe refrescar_presenca(), que hace un listdir por cada carpeta con anexos.

Prácticas

  1. En test_reindex_incremental.py, busca el test que cuenta cuántos saber.bbx se releyeron. Cambia un saber y comprueba el número.
  2. Corrompe a mano el YAML de un saber.bbx (como lo haría un conflicto de merge) y corre bbx reindex. ¿El saber desaparece de la búsqueda? ¿Por qué no? (_load, reindex.)
  3. Abre el índice con sqlite3 .bbx/index.sqlite y corre SELECT nature, count(*) FROM acesso GROUP BY nature;.
  4. Lee _index_entry y explica de dónde viene la columna territorio de un saber que no declaró territorio.

Para conversar

  • El índice puede borrarse en cualquier momento. ¿Qué libertad da eso a quienes mantienen el sistema? ¿Qué cuidado exige?

ver este capítulo como saber