diff --git a/CLAUDE.md b/CLAUDE.md index bd94be3..637d39e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -250,20 +250,47 @@ Compilation **locale**, puis dépôt manuel de l'archive en release sur le Gitea **Ne jamais compiler sur le serveur** à l'installation : cela imposerait le SDK .NET complet sur la machine YunoHost, pour une compilation lente — l'inverse exact de ce que permet le self-contained. -## Modèle de données (base de départ, à affiner) +## Modèle de données + +Schéma en vigueur depuis la migration `StatutPersonnelEtTableAuteurs` (2026-08-18). ``` Livre ├── Id -├── Isbn +├── Isbn (index) ├── Titre -├── Auteur +├── TitreNormalise (index — minuscules sans accents, recalculé à chaque écriture) ├── Editeur ├── Format : Physique | Numerique -├── Statut : ALire | EnCours | Lu ├── CoverUrl ├── DateAjout └── AjoutePar (YNH_USER — traçabilité, PAS un cloisonnement) + ⚠ pas de colonne Statut, pas de colonne Auteur + +Auteur +├── Id +├── Nom (affichage : « Émile Zola ») +├── NomNormalise (index — recherche : « emile zola ») +└── CleRegroupement (index UNIQUE — mots normalisés triés : « emile zola ») + +LivreAuteur (n-n : un livre peut avoir plusieurs auteurs) +├── LivreId ─┐ clé primaire composite +├── AuteurId ─┘ FK Livre = cascade, FK Auteur = restrict +└── Position (rang sur la couverture, à partir de 0) + +StatutLecture (le statut est PERSONNEL) +├── Id +├── LivreId (FK vers Livre, cascade) +├── Utilisateur (YNH_USER) +├── Statut : ALire | EnCours | Lu +├── DateMaj +└── UNIQUE (LivreId, Utilisateur) + absence de ligne = « non commencé » + +RapprochementRefuse (mémoire des « non » de l'utilisateur) +├── Id +├── AuteurAId ─┐ UNIQUE, couple toujours rangé (petit Id d'abord) +└── AuteurBId ─┘ pour qu'un refus vaille dans les deux sens Pret ├── Id @@ -281,6 +308,26 @@ Garder `Pret` comme table séparée (pas un champ sur `Livre`) pour conserver l' **L'auteur n'est plus un champ texte sur `Livre`.** Une table dédiée porte le nom d'affichage et une forme normalisée servant au regroupement et à la recherche. Le regroupement automatique ne s'applique qu'aux variantes **sûres** (casse, accents, initiales, ordre nom/prénom) ; les rapprochements ambigus — « Hamilton » seul vers « Peter F. Hamilton » — doivent être **proposés**, jamais appliqués silencieusement : une fusion erronée est difficile à défaire. +### Recherche sans accents : pourquoi des colonnes normalisées + +**SQLite ne sait pas ignorer les accents.** Sa collation `NOCASE` ne couvre que l'ASCII et il n'existe pas de `unaccent()` intégré. La seule façon d'obtenir « Emile » → « Émile » sans charger tout le catalogue en mémoire est de stocker la forme mise à plat dans une colonne dédiée, et d'y chercher un terme passé par **exactement la même** fonction (`NormalisationTexte`). + +Conséquence à ne pas oublier : **toute écriture doit recalculer ces colonnes** (`Livre.RecalculerFormes()`, `Auteur.RecalculerFormes()`). + +Deux formes cohabitent sur `Auteur`, et ce n'est pas une redondance : + +- `NomNormalise` garde l'ordre de saisie — c'est ce que la recherche parcourt en sous-chaîne ; +- `CleRegroupement` trie les mots, ce qui rend `Zola, Émile` et `Émile Zola` identiques. C'est elle qui porte l'index **unique**, donc l'invariant « un auteur, une fiche ». + +La clé ne sait pas rapprocher les **initiales** (`P.F. Hamilton` ↔ `Peter F. Hamilton`) : cela demande d'aligner les mots un à un, ce qui n'a pas d'équivalent SQL. Ce cas est traité en mémoire par `RapprochementAuteurs.Comparer`, sur une table qui compte au plus quelques centaines de lignes. + +### Reprise des données existantes + +La migration recopie l'ancien contenu avec les moyens du bord, puis **`ServiceRenormalisation` finit le travail en C# au démarrage** — idempotent, il sert aussi de filet si les règles de normalisation changent un jour. + +- L'ancien `Livres.Statut`, commun à tout le foyer, est **rattaché à `AjoutePar`**, seule personne que la base associe au livre. Les statuts des livres sans `AjoutePar` sont **perdus** : les attribuer serait une invention. +- L'ancien `Livres.Auteur` devient une fiche par valeur distincte. SQLite ne regroupant qu'à la casse près, les variantes accentuées ressortent en double — c'est le passage C# qui les réunit, en gardant le nom d'affichage le plus présentable (accents conservés, ni `NOM, Prénom` ni capitales intégrales). + **Les prêts ne concernent en pratique que les livres physiques** — les ebooks étant de simples fiches, il n'y a pas d'objet à prêter. `Emprunteur` reste un **texte libre**, sans lien avec les comptes YunoHost : on suit les prêts à des personnes extérieures au foyer, pas les échanges entre utilisateurs de l'app. ## Historique du projet (pourquoi ces choix)