gradatum-core 0.4.3

Shared primitives: errors, IDs, types
Documentation
//! Trait d'abstraction de l'index Gradatum.
//!
//! ## Design
//!
//! `Index` est une **façade supertrait** qui compose les 3 traits granulaires :
//! - [`DocumentStore`] — CRUD notes
//! - [`IndexStore`] — FTS, overrides, checksums, scoring composite
//! - [`VectorStore`] — embeddings + recherche sémantique cosine
//!
//! Le trait `Index` vit dans `gradatum-core` (pas dans `gradatum-index`) — décision Q5DAG :
//! les crates consommateurs (`gradatum-vault`, `gradatum-curator`) importent ce trait abstrait,
//! pas l'implémentation concrète `SqliteIndex` (dans `gradatum-index`).
//!
//! Avantage : `gradatum-vault` ne dépend pas de `gradatum-index` → pas de cycle.
//!
//! ## Compat backward
//!
//! `Index as _` reste fonctionnel pour les consommateurs existants. Pour accéder aux
//! méthodes granulaires (ex. `list_file_checksums`), importer également le sous-trait
//! correspondant (`IndexStore as _`, `DocumentStore as _`, `VectorStore as _`).
//!
//! ## FileChecksumEntry
//!
//! Entrée de la table `file_checksums` — drift detection fichier par fichier.
//! Permet de détecter un fichier modifié hors de Gradatum sans re-hasher tout le vault.
//! Stratégie : (1) vérification rapide mtime + size, (2) hash partiel 4KB, (3) hash complet.
//!
//! ## Overrides génériques
//!
//! `upsert_override_raw` / `get_override_raw` stockent tout payload override en TOML
//! dans la table générique `note_overrides` (décision Q7/B20).
//! Le schéma est identifié par (`override_type`, `schema_version`) → validation via
//! `gradatum-core::schema_registry`.
//!
//! ## Méthodes restées concrètes hors-trait (SqliteIndex uniquement)
//!
//! Les méthodes suivantes de `SqliteIndex` ne sont PAS promues en trait à v0.3.0 :
//! - `search_fts_with_snippet` : retourne `SearchHitRaw` (type dans `gradatum-index`) — promotion
//!   différée v0.4.0 (blast radius type public).
//! - Méthodes admin/lifecycle (`downgrade_note`, `patch_note_status`, `list_notes`, etc.) :
//!   sémantique opérationnelle, pas de consommateur trait-based prévu à v0.3.0.
//! - Méthodes seed/bench : utilitaires test/perf, hors contrat public.

use serde::{Deserialize, Serialize};

use crate::document_store::DocumentStore;
use crate::index_store::IndexStore;
use crate::vector_store::VectorStore;

/// Façade historique — combinaison des 3 traits de storage.
///
/// Conservée pour la compat des call sites existants en v0.3.0.
/// Les nouveaux consommateurs DOIVENT dépendre des traits granulaires
/// (`DocumentStore` / `IndexStore` / `VectorStore`) — plus précis et évolutifs.
///
/// ## Blanket impl
///
/// Tout type qui implémente les 3 sous-traits est automatiquement un `Index`.
/// Aucun `impl Index for T` manuel n'est nécessaire.
pub trait Index: DocumentStore + IndexStore + VectorStore {}

/// Blanket impl : tout type implémentant les 3 sous-traits devient un `Index`.
impl<T: DocumentStore + IndexStore + VectorStore + ?Sized> Index for T {}

// ── F-55 TemporalIndex ────────────────────────────────────────────────────────

/// Source de l'ancre temporelle dans la table `temporal_index`.
///
/// Encodée en tant que `&'static str` dans la DB (colonne `anchor_src`) pour lisibilité.
/// Ordre de priorité décroissant lors de la résolution d'une ancre :
/// `OccurredAt > EventDate > ValidFrom > Created`.
///
/// Fallback : `Created` est toujours disponible (colonne `notes.created` NOT NULL).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AnchorSrc {
    /// Champ `occurred_at` trouvé dans `frontmatter.extra` (ExtraFields YAML).
    #[serde(rename = "occurred_at")]
    OccurredAt,
    /// Champ `event-date` trouvé dans `frontmatter.extra` (ExtraFields YAML).
    #[serde(rename = "event-date")]
    EventDate,
    /// Champ `valid_from` trouvé dans `frontmatter.extra` (ExtraFields YAML).
    #[serde(rename = "valid_from")]
    ValidFrom,
    /// Fallback : timestamp `notes.created` (toujours disponible).
    #[serde(rename = "created")]
    Created,
}

impl AnchorSrc {
    /// Représentation textuelle stable pour stockage en DB.
    ///
    /// Ces chaînes correspondent aux valeurs de la contrainte CHECK dans la migration 0013.
    pub fn as_db_str(self) -> &'static str {
        match self {
            Self::OccurredAt => "occurred_at",
            Self::EventDate => "event-date",
            Self::ValidFrom => "valid_from",
            Self::Created => "created",
        }
    }
}

/// Entrée de la table `temporal_index` — ancre temporelle d'une note.
///
/// ## Design
///
/// Table dérivée — toutes les données sont calculables depuis `notes` + frontmatter.
/// Pas de surface API/MCP en v0.4.3 (vault_timeline = v0.5.0).
///
/// ## Caveat C7 (PRAGMA foreign_keys non garanti)
///
/// La colonne `note_id` est une référence logique, PAS une FOREIGN KEY.
/// La suppression doit être explicite via `DELETE FROM temporal_index WHERE note_id = ?`
/// dans `delete_note_from_index` — ne pas compter sur ON DELETE CASCADE.
///
/// ## Ancre temporelle (priorité décroissante)
///
/// 1. `occurred_at` dans `frontmatter.extra`
/// 2. `event-date` dans `frontmatter.extra`
/// 3. `valid_from` dans `frontmatter.extra`
/// 4. `notes.created` (fallback universel)
///
/// ## `valid_until_ms`
///
/// Réservé pour le fenêtrage temporel v0.5.0. `None` en v0.4.3.
#[derive(Debug, Clone, PartialEq)]
pub struct TemporalEntry {
    /// ULID de la note (clé primaire de `temporal_index`).
    pub note_id: String,

    /// Tenant de la note.
    pub vault_id: String,

    /// Ancre temporelle en epoch UTC millisecondes.
    pub anchor_ms: i64,

    /// Source utilisée pour calculer `anchor_ms`.
    pub anchor_src: AnchorSrc,

    /// Axe temporel CoALA de la note (`"Static"` | `"Event"` | `"Versioned"`).
    ///
    /// Dérivé de `notes.doc_kind` (migration 0008). `"Versioned"` réservé v0.5.0.
    pub doc_kind: String,

    /// Borne supérieure optionnelle en epoch UTC ms (réservé fenêtrage v0.5.0).
    pub valid_until_ms: Option<i64>,
}

/// Entrée de drift detection par fichier.
///
/// Stockée dans la table `file_checksums`. Permet de détecter les modifications
/// hors-Gradatum en vérifiant (mtime + size) avant de re-hasher le fichier entier.
#[derive(Debug, Clone, PartialEq)]
pub struct FileChecksumEntry {
    /// Chemin relatif depuis la racine du vault (ex. `"decisions/2026-05-04-my-note.md"`).
    pub relative_path: String,

    /// Type de fichier.
    pub file_kind: FileKind,

    /// Taille attendue en bytes.
    pub expected_size: u64,

    /// Hash SHA-256 des 4 premiers KB (vérification rapide avant hash complet).
    ///
    /// Évite de lire tout un gros fichier pour détecter qu'il n'a pas changé.
    pub expected_hash_prefix_4kb: [u8; 32],

    /// Hash SHA-256 complet du fichier.
    pub expected_hash: [u8; 32],

    /// mtime Unix epoch (secondes) attendu.
    pub expected_mtime: i64,

    /// Timestamp Unix epoch de la dernière vérification réussie.
    pub last_verified: i64,
}

/// Catégorie de fichier trackée dans `file_checksums`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FileKind {
    /// Fichier Markdown d'une note.
    Note,
    /// Fichier TOML d'un override.
    Override,
    /// Fichier de configuration du vault.
    Config,
}

/// Enregistrement complet d'une note retourné par [`DocumentStore::get_note`].
///
/// Struct portable défini dans `gradatum-core` pour permettre son usage
/// via le trait `DocumentStore` sans dépendance vers `gradatum-index` (décision Q5DAG).
#[derive(Debug, Clone)]
pub struct NoteRecord {
    /// Identifiant ULID de la note.
    pub id: String,
    /// Identifiant du vault.
    pub vault_id: String,
    /// Section thématique (ex. `"decisions"`, `"architecture"`).
    pub section: String,
    /// Statut de la note (ex. `"live"`, `"pending-review"`).
    pub status: String,
    /// Corps Markdown complet.
    pub body_text: String,
    /// Auteur de la note (display name ou id, peut être absent).
    pub author: Option<String>,
    /// Tags espace-séparés (depuis `notes.tags`, migration 0003).
    pub tags_raw: Option<String>,
    /// Hash de contenu SHA-256 (32 bytes) — pour calcul hex dans les handlers.
    pub content_hash: Vec<u8>,
    /// Timestamp de création (epoch ms).
    pub created: i64,
    /// Timestamp de dernière mise à jour (epoch ms, peut être absent).
    pub updated: Option<i64>,
    /// Titre H1 Markdown de la note (extrait au curate, peut être absent).
    ///
    /// Peuplé par la migration 0005 (backfill) et mis à jour à chaque curate.
    /// `None` si la note n'a pas de ligne `# ...` en première position.
    pub title: Option<String>,
}