# SPEC — Annotations éditoriales (YAML v1) + merge + anti-doublon > Objectif : permettre aux tickets (Gitea) de déposer “Références / Médias / Commentaires” dans `src/annotations/**`, > de façon univoque, stable, et sans régression. ## 0) Contexte et intention Le site est statique. L’édition collaborative se fait via : - un mode “proposition” (UI / modal) - un ticket Gitea (issue) standardisé - un script d’application côté éditeur (`apply-ticket.mjs` ou équivalent) - génération d’un YAML d’annotations versionné dans Git La donnée d’annotation doit être : - **audit-able** (Git) - **merge-able** (sans tout casser) - **stable** (IDs paragraphes / liens / médias) - **scalable** (éviter YAML monstrueux à long terme) ## 1) Arborescence canonique ### 1.1 Un workKey par “ouvrage / section du site” On veut une univocité entre : - SiteNav (Méthode, Essai-thèse, Traité, Cas IA, Glossaire, Atlas) et - l’arborescence annotations Proposition canonique (workKey = route racine) : - `methode` - `archicrat-ia` (Essai-thèse ArchiCraT-IA) - `traite` - `ia` - `glossaire` - `atlas` ### 1.2 Règle de stockage “v1” **Par page**, un YAML unique : src/annotations//.yml Exemples : - Page : `/archicrat-ia/prologue/` - slug content = `archicrat-ia/prologue` - fichier : `src/annotations/archicrat-ia/prologue.yml` - Page : `/traite/00-demarrage/` - fichier : `src/annotations/traite/00-demarrage.yml` > Note : “slugSansWorkKey” = la partie après `/`. > S’il y a des sous-dossiers (chapitres), le chemin reflète la structure : `chapitre-1/section-a.yml` si on choisit du sharding. ## 2) Question “gros YAML” : page unique vs sharding par paragraphe ### 2.1 Option A (v1 recommandée) : 1 YAML par page Avantages : - simple - peu de fichiers - diff lisible si volume modéré - cohérent avec un modèle “annotations par page” Inconvénients : - YAML peut grossir si milliers d’annotations ### 2.2 Option B (v2 future) : sharding par paragraphe src/annotations///.yml Avantages : - fichiers petits - merges moins conflictuels Inconvénients : - plus de fichiers - tooling plus complexe (indexation + merge multi-fichiers) ### 2.3 Recommandation de mission (sans casser l’existant) - On démarre en **Option A**. - On se garde une migration future (v2) quand le volume réel le justifie. - On impose dès v1 : **clé unique + merge déterministe + anti-doublon**, ce qui rend la migration future possible. ## 3) Format YAML v1 (schéma complet) ### 3.1 Top-level en yaml : schema: 1 # Optionnel mais recommandé (doit matcher la page) page: "/" meta: title: "Titre de la page (optionnel)" updatedAt: "2026-02-21T12:34:56Z" # ISO8601 updatedBy: "username" # compte editor source: kind: "ticket" id: 123 url: "https://gitea.../issues/123" paras: "": references: [] media: [] comments: [] ### 3.2 paras : clé = paraId (ex: p-0-d7974f88) Chaque paragraphe peut porter 3 types d’éléments : references media comments Règle : si une section est vide, elle peut être [] ou absente. Mais pour simplifier les merges, on recommande de garder la forme canonique avec []. ## 4) Formats des items + clés uniques ### 4.1 References #### 4.1.1 Format references: - id: "ref:doi:10.1234/abcd.efgh" # clé stable (voir 4.1.2) kind: "doi" # doi | url | isbn | arxiv | hal | other label: "Titre court" target: "https://doi.org/10.1234/abcd.efgh" note: "Pourquoi c’est pertinent (optionnel)" addedAt: "2026-02-21T12:34:56Z" addedBy: "username" #### 4.1.2 Règle de clé unique (anti-doublon) id doit être stable et déterministe : doi → ref:doi: isbn → ref:isbn: url → ref:url: Normalisation URL (v1) : au minimum trim lowercase scheme/host retirer trailing slash si non significatif conserver query si importante #### 4.1.3 Merge / précédence Quand on merge deux listes references : union par id (clé unique) si même id existe des deux côtés : conserver kind/target de l’item le plus “riche” (target non vide gagne) concat/merge note : si notes différentes : garder les deux en les séparant (ex: noteA + "\n---\n" + noteB) addedAt : conserver le plus ancien addedBy : conserver le premier (ou liste si on veut, mais v1 simple : first) ### 4.2 Media #### 4.2.1 Format media: - id: "media:image:sha256:abcd..." # clé stable (voir 4.2.2) type: "image" # image | video | audio | file src: "/public/media////" caption: "Légende (optionnel)" credit: "Auteur/source (optionnel)" license: "CC-BY (optionnel)" addedAt: "2026-02-21T12:34:56Z" addedBy: "username" #### 4.2.2 Règle de clé unique id déterministe : idéal : hash du fichier (sha256) sinon : hash de type + src v1 (si on ne calcule pas de hash fichier) : media:: #### 4.2.3 Merge / précédence union par id si collision : garder src identique (sinon c’est un bug) fusionner caption/credit/license selon “non vide gagne” addedAt : plus ancien ### 4.3 Comments #### 4.3.1 Format comments: - id: "cmt:20260221T123456Z:username:0001" kind: "comment" # comment | question | objection | todo | validation text: "Texte du commentaire" status: "open" # open | resolved addedAt: "2026-02-21T12:34:56Z" addedBy: "username" source: kind: "ticket" id: 123 #### 4.3.2 Clé unique Les commentaires sont “append-only” → id peut être générée (timestamp + user + compteur) Anti-doublon : si on ré-applique un ticket, on refuse de dupliquer un id existant. #### 4.3.3 Merge / précédence union par id collisions rares, mais si elles arrivent : si textes différents → garder les deux (on renomme l’id du second) ## 5) Règles globales de merge (résumé) Quand on applique un ticket sur un YAML existant : vérifier schema == 1 vérifier page si présent : doit matcher / paras : créer paras[paraId] si absent pour chaque liste (references/media/comments) : merge par id (anti-doublon) appliquer règles de précédence (non vide gagne / concat note / append-only comments) ## 6) Table de correspondance “UI ticket → YAML” Cette table permet à un successeur IA d’implémenter apply-ticket.mjs sans ambiguïté. ### 6.1 Champs UI minimaux workKey (sélection implicite via page) pagePath (ex: /archicrat-ia/prologue/) pageSlug (ex: archicrat-ia/prologue) paraId (ex: p-0-d7974f88) kind : reference media comment ### 6.2 Mapping exact | UI kind | UI champs | YAML cible | | --------- | ----------------------------------------------------------- | ---------------------------- | | reference | kind(doi/url/isbn), target, label, note | `paras[paraId].references[]` | | media | type(image/video/audio/file), src, caption, credit, license | `paras[paraId].media[]` | | comment | kind(comment/question/objection/todo/validation), text | `paras[paraId].comments[]` | ### 6.3 Règles de génération d’ID (implémentation) reference.id : doi : ref:doi:${doi} isbn : ref:isbn:${isbn} url : ref:url:${normalize(url)} media.id : media:${type}:${src} comment.id : cmt:${timestamp}:${user}:${counter} ## 7) Validation YAML (sanity) Avant commit (et en CI) : YAML parse OK schema OK page si présent cohérent paras est un mapping paraId match pattern : ^p-\d+-[a-f0-9]{8}$ (existant) src media pointe dans /public/media/... (ou /media/... si on choisit un alias, mais v1 canon : /public/media/...) ## 8) Notes de compatibilité Les routes “Essai-thèse” ont été migrées vers /archicrat-ia/*. Les anciennes routes /archicratie/archicrat-ia/* peuvent exister en legacy, mais la donnée canonique d’annotation doit suivre le workKey final (archicrat-ia). ## 9) Ce que l’étape 9 devra implémenter pipeline : ticket → YAML (apply-ticket) index : build-annotations-index + check-annotations tooling : détection médias orphelins / liens cassés éventuellement : migration vers sharding par paragraphe (v2) si volume réel le justifie