app/ledger/) est la couche de capture durable de Haki :
il écrit des événements (la preuve brute) et gère les faits (ce qui
est vrai), avec une interface volontairement petite — écrire un événement,
lire un objet, lister une timeline, appliquer une mutation de statut
versionnée.
Événement (Event)
Un événement est la preuve : « ce message a été dit à cette date ».
Table events (app/models/event.py), append-only — le contenu
métier n’est jamais modifié après l’insertion. La seule écriture tolérée
après coup est l’embedding dérivé (mémoire épisodique, calculé une fois
par le Consolidator).
string
requis
Le périmètre complet de l’événement.
string
défaut:"user"
Type du sujet (client, utilisateur…).
string
requis
Identité stable du sujet. Obligatoire — validé par le Ledger (erreur
typée
missing_scope), pas par Pydantic, pour produire un message
d’erreur exact avec l’index de l’événement fautif dans le batch.string | null
Provenance : qui/quoi a produit l’événement, dans quel fil de
conversation, dans quelle exécution.
string
requis
Type libre, ex.
conversation.message, conversation.turn,
agent.observation. Aucune énumération fermée côté serveur.datetime
requis
Temps métier — quand l’événement s’est vraiment produit.
datetime
Temps système — quand Haki l’a enregistré. Généré côté serveur
(
server_default=func.now()).object
requis
Contenu libre (JSONB).
string[]
Étiquettes libres, ex.
["customer-data"].string
sha256: + hash canonique du contenu métier (org/project/subject/kind/
occurred_at/payload, clés triées). Calculé côté serveur, pas transmis
par le client.Bitemporalité
Haki distingue systématiquement deux axes de temps :
Cette séparation permet de répondre à deux questions différentes : « que
savait-on à la date X ? » (temps système) et « qu’est-ce qui était vrai à
la date X ? » (temps métier) — sans jamais les confondre. Un fait
deleted (terminal) obtient recorded_to au moment de la transition ;
valid_to marque, lui, la fin de validité métier (posée par le
Consolidator au moment d’une supersession).
Idempotence à l’écriture
write_events (app/ledger/core.py) insère par lot avec
ON CONFLICT DO NOTHING sur la contrainte unique
(project_id, idempotency_key). La clé effective de chaque événement est
choisie dans cet ordre :
idempotency_keydu batch (CaptureRequest.idempotency_key), namespacé par le hash de contenu de chaque événement (f"{batch_key}:{content_hash}") — plusieurs événements dans un même lot ne collisionnent jamais entre eux ;- sinon
idempotency_keypropre à l’événement (EventIn.idempotency_key) ; - sinon un fallback dérivé du hash de contenu +
subject_id.
POST /v1/capture renvoie les mêmes
identifiants d’événements, avec deduplicated: true sur ceux déjà connus.
Timeline
GET /v1/timeline?project_id=...&subject_id=... (les deux paramètres sont
obligatoires — jamais de timeline cross-sujet) renvoie les événements
ordonnés par (occurred_at, recorded_at). C’est la vue « preuve brute »
utilisée par la console et par haki_inspect côté MCP.
Référence API — Capture
Schéma exact de
POST /v1/capture, exemples de requête/réponse.
