Parte II — Tronco e radici
03 · Il tronco: dove si conserva l'acqua
Il baobab accumula fino a migliaia di litri d'acqua nel tronco e attraversa mesi di siccità. Il nucleo di Baobáxia conserva i saberes in file semplici, che sopravvivono a blackout, dischi sostituiti e anni senza rete.
File da leggere, in quest'ordine
baobaxia/core/saber.py— il modello (≈ 250 righe).baobaxia/infra/storage.py— leggere e scriveresaber.bbx.baobaxia/core/repository.py—SaberRepository: una galassia.baobaxia/core/index.py—GalaxyIndex: la cache SQLite.baobaxia/core/relation_graph.pyecore/registry.py.
Il file 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}
---
Testo libero in markdown (diventa data.body).
Leggibile dalle persone, versionabile da git, e foto.jpg è un link simbolico di git-annex: il binario esiste solo nelle mucua che ne hanno chiesto una copia.
SaberRepository: una galassia
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 {
<<facciata>>
+init(path)
+add(path, root)
+commit(root, msg)
+sync(root, remote, data) bool
+get / drop / whereis
}
SaberRepository *-- GalaxyIndex
SaberRepository ..> storage
SaberRepository ..> Sankofa
Scrivere 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) e validate_data (schema della nature)
S->>S: authorship.try_sign (firma con la chiave dell'autore, se c'è)
S->>R: create(saber, author)
R->>R: applicazioni predefinite della nature, slug, cartella
R->>R: _merge_content (ref dichiarati + 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 il grafo e propaga alle copie in altre galassie
L'indice SQLite (core/index.py)
Una tabella larga (saberes), tabelle ausiliarie e una tabella stretta acesso mantenuta da trigger — gli elenchi verificano i permessi di migliaia di righe senza leggere i blob 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 "minuscolo e senza accenti"
TEXT meta_json
TEXT data_json
TEXT fs_sig "mtime:dimensione"
INTEGER is_local
}
acesso {
TEXT uuid PK
TEXT protecao
TEXT leitores
TEXT editores
TEXT status
INTEGER ok "json valido"
}
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 : ""
Dettagli che vale la pena leggere:
- Lettori separati (
_Leitores,_leitura("rapida"|"pesada")): una query veloce (per uuid) non aspetta mai un elenco pesante. geracao(): un contatore globale delle scritture; la cache di lettura (services/cache_leitura.py) usa questo numero come chiave — qualsiasi scrittura invalida tutto in un colpo.- Ricerca senza accenti:
normalizar_busca(NFKD + minuscole) da entrambi i lati. "acai" trova "açaí".
Reindex incrementale: il tempo a spirale
reindex(incremental=True) chiede a git cosa è cambiato dall' ultimo_commit_indexado (_pastas_mudadas_no_git) e rilegge solo quelle cartelle. Senza marcatore, confronta la firma su disco (fs_sig). Il marcatore viene scritto nella stessa transazione dei dati: se andasse avanti, dei saberes sparirebbero per sempre.
flowchart TD
A[reindex incrementale] --> B{il marcatore esiste ed è<br/>antenato di HEAD?}
B -- sì --> C[git diff marcatore..HEAD<br/>→ cartelle toccate]
C --> D[rilegge solo quelle cartelle<br/>+ righe con fs_sig vuoto]
B -- no --> E[scorre i saber.bbx sul disco<br/>apre solo quelli con fs_sig diverso]
D --> F[aplicar_lote: voci + rimozioni + marcatore<br/>in una transazione]
E --> F
F --> G[saber.bbx illeggibile?<br/>mantiene la versione precedente e avvisa nel log]
La presenza di un binario (git annex get/drop) non cambia nessun file versionato — per questo esiste refrescar_presenca(), che fa un listdir per ogni cartella con allegati.
Esercizi
- In
test_reindex_incremental.py, trova il test che conta quantisaber.bbxsono stati riletti. Cambia un saber e verifica il numero. - Corrompi a mano lo YAML di un
saber.bbx(come farebbe un conflitto di merge) ed eseguibbx reindex. Il saber sparisce dalla ricerca? Perché no? (_load,reindex.) - Apri l'indice con
sqlite3 .bbx/index.sqliteed eseguiSELECT nature, count(*) FROM acesso GROUP BY nature;. - Leggi
_index_entrye spiega da dove viene la colonnaterritoriodi un saber che non ha dichiarato un territorio.
Da discutere
- L'indice può essere cancellato in qualsiasi momento. Che libertà dà a chi mantiene il sistema? Che cura richiede?
