# 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