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.
Archivos para leer, en este orden
baobaxia/core/saber.py— el modelo (≈ 250 líneas).baobaxia/infra/storage.py— leer y escribirsaber.bbx.baobaxia/core/repository.py—SaberRepository: una galaxia.baobaxia/core/index.py—GalaxyIndex: la caché SQLite.baobaxia/core/relation_graph.pyycore/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
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
- En
test_reindex_incremental.py, busca el test que cuenta cuántossaber.bbxse releyeron. Cambia un saber y comprueba el número. - Corrompe a mano el YAML de un
saber.bbx(como lo haría un conflicto de merge) y correbbx reindex. ¿El saber desaparece de la búsqueda? ¿Por qué no? (_load,reindex.) - Abre el índice con
sqlite3 .bbx/index.sqlitey correSELECT nature, count(*) FROM acesso GROUP BY nature;. - Lee
_index_entryy explica de dónde viene la columnaterritoriode 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?
