# Contrat des ancres (paragraphes opposables)

## Source de vérité du sélecteur
Le site garantit la citabilité des paragraphes via des IDs injectés sur les balises `<p>`.

**Sélecteur contractuel :**
- `.reading p[id^="p-"]`

Tout outillage (scripts, tests, docs) doit utiliser ce sélecteur comme référence.

## Ce que le test vérifie
Le test compare, page par page, la liste des IDs de paragraphes présents dans `dist/` contre une baseline versionnée.

- Ajouts d’IDs : généralement OK (nouveaux paragraphes).
- Suppressions / churn élevé : alerte (risque de casser des citations existantes).

## Fichier baseline
- `tests/anchors-baseline.json`

## Commandes
1) Générer / mettre à jour la baseline (cas intentionnel) :
- `npm run build`
- `npm run test:anchors:update`

2) Vérifier sans changer la baseline (cas normal) :
- `npm run build`
- `npm run test:anchors`

## Politique d’échec (pragmatique)
Le test échoue si le churn d’une page dépasse un seuil (défaut : 20%) sur une page “suffisamment grande”.

## Aliases build-time
- `src/anchors/anchor-aliases.json`
- `scripts/inject-anchor-aliases.mjs`
- `scripts/check-anchor-aliases.mjs`
- et rappelle : *alias = compat rétro de liens historiques sans JS*

## Ancres de section (H2) quand on utilise `<details>`
Quand un chapitre est structuré en sections repliables (`<details>`), on utilise une ancre “technique” dédiée pour garantir :
- une cible stable même si le H2 est dans un bloc replié,
- un scroll-offset correct (header + bandeau),
- une détection fiable pour le bandeau “reading-follow”.

Contrat côté HTML rendu :
- présence d’un élément de type :
  - `<span class="details-anchor" id="..."></span>`
- puis, dans le même `<details>`, un H2 visible dans le body.

Contrat côté CSS :
- `.details-anchor` a `scroll-margin-top: var(--sticky-offset)`.

Contrat côté JS (EditionLayout) :
- `openDetailsIfNeeded(el)` ouvre le `<details>` parent si nécessaire avant de scroller.

## Compat “legacy hash” : `#p-<idx>-<hash>`
Un hotfix de compat existe pour les anciennes ancres de paragraphes au format :
- `#p-<index>-<8 hex>`

Si l’ID exact n’existe plus :
- on cherche le premier élément dont l’id commence par `p-<index>-`
- puis scroll avec offset.

But : éviter les “liens morts” historiques quand une régénération d’IDs a eu lieu.

Limite : c’est un fallback de dernier recours (moins déterministe qu’un alias explicite).
Le mécanisme recommandé reste : `src/anchors/anchor-aliases.json` + injection au build.

_______________________________________

Dernière mise à jour : 2026-01-29

Ce document formalise comment on évite de casser les liens profonds (URLs avec `#ancre`).

---

## 1) Principe

Les IDs d’ancres générés (ou dérivés) peuvent changer :
- réécriture
- insertion/suppression de paragraphes
- re-slug d’un titre

➡️ Donc : on ne “devine” pas un fallback par index en runtime.
➡️ On fait un aliasing **déterministe à la build**, versionné.

---

## 2) Le mapping d’alias

- Fichier versionné (ex) : `src/anchors/anchor-aliases.json`
- Format : `oldId -> newId` par page

Ex en json :
{
  "/archicratie/archicrat-ia/chapitre-4/": {
    "p-8-ancien": "p-8-nouveau"
  }
}

## 3) Injection à la build (dans dist)

Script : scripts/inject-anchor-aliases.mjs

Moment : postbuild (après astro build, avant pagefind)

Ce script injecte dans dist/**/index.html un <span id="oldId"></span> juste avant l’élément portant id="newId".

## 4) Contrôles (incassables)
### A) Vérifier qu’on n’a pas d’IDs dupliqués en HTML

Script : scripts/audit-dist.mjs

Robustesse :

ignore <script> et <style> (évite faux positifs)

exige un espace avant id= (évite data-id=)

Exécution :
npm run audit:dist

### B) Vérifier que les aliases sont vraiment présents dans dist

Script : scripts/verify-anchor-aliases-in-dist.mjs

Exécuté dans npm test

## 5) Mise à jour assistée (si besoin)

Pour générer/mettre à jour des aliases à partir des deltas d’ancres :
npm run test:anchors:update

## 6) ⚠️ Artefacts macOS (PaxHeader / ._ / DS_Store)

Quand on transfère une archive depuis macOS, on peut importer des dossiers/fichiers parasites :

PaxHeader/

._*

.DS_Store

Ces artefacts peuvent polluer le build (Astro croit voir un “vrai” contenu MDX).

➡️ Recommandation : .dockerignore strict :
# macOS / archives
.DS_Store
._*
**/._*
PaxHeader
**/PaxHeader

# Node
node_modules
dist

# (Optionnel) Avant build sur NAS : supprimer ces dossiers si présents.
