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.

Il frutto della mucua: come una galassia vive sul disco

File da leggere, in quest'ordine

  1. baobaxia/core/saber.py — il modello (≈ 250 righe).
  2. baobaxia/infra/storage.py — leggere e scrivere saber.bbx.
  3. baobaxia/core/repository.py — SaberRepository: una galassia.
  4. baobaxia/core/index.py — GalaxyIndex: la cache SQLite.
  5. baobaxia/core/relation_graph.py e core/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

Tempo a spirale: cronologia e reindex

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

  1. In test_reindex_incremental.py, trova il test che conta quanti saber.bbx sono stati riletti. Cambia un saber e verifica il numero.
  2. Corrompi a mano lo YAML di un saber.bbx (come farebbe un conflitto di merge) ed esegui bbx reindex. Il saber sparisce dalla ricerca? Perché no? (_load, reindex.)
  3. Apri l'indice con sqlite3 .bbx/index.sqlite ed esegui SELECT nature, count(*) FROM acesso GROUP BY nature;.
  4. Leggi _index_entry e spiega da dove viene la colonna territorio di 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?

ver este capítulo como saber