327 lines
8.2 KiB
Markdown
327 lines
8.2 KiB
Markdown
# 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/<workKey>/<slugSansWorkKey>.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 `<workKey>/`.
|
||
> 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/<workKey>/<slugSansWorkKey>/<paraId>.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: "<workKey>/<slugSansWorkKey>"
|
||
|
||
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:
|
||
"<paraId>":
|
||
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:<doi>
|
||
|
||
isbn → ref:isbn:<isbn>
|
||
|
||
url → ref:url:<normalizedUrl>
|
||
|
||
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/<workKey>/<slugSansWorkKey>/<paraId>/<filename>"
|
||
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:<type>:<src>
|
||
|
||
#### 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 <workKey>/<slugSansWorkKey>
|
||
|
||
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 |