# Manuel de référence — Archicratie Web Edition (Site + Gitea + Runner)

Ce manuel explique comment utiliser et maintenir l’outil d’édition :
- Site Astro “Archicratie – Web Edition”
- Proposer (tickets Gitea pré-remplis)
- Apply-ticket (application semi-automatique dans le contenu)
- Contrat d’ancres (citabilité) + tests
- CI Gitea Actions + ds220-runner (DS220+)

> Objectif : une boucle éditoriale reproductible, opposable, sûre, et testée.

---

## 0) Glossaire minimal (anti-jargon)
- **PR** (Pull Request) : demande de fusion d’une branche vers `master`.
- **PAT** (Personal Access Token) : jeton d’accès Gitea utilisé comme “mot de passe” pour API/Git.
- **Anchor / Ancre** : identifiant stable d’un paragraphe (ex: `p-8-0e65838d`) utilisable dans une URL `#...`.
- **Churn** : variation des ancres d’une page entre deux builds (mesure de stabilité).
- **dist/** : sortie buildée (artefact), jamais la “source de vérité”.
- **CI** : automatisation (build + tests) à chaque push/PR.

---

## 1) Pré-requis (poste local)
### Outils
- Node.js + npm
- Git
- (Optionnel) Python3 pour scripts de debug ponctuels

### Accès
- Compte Gitea avec droits sur le repo
- Un **PAT** Gitea
- Accès au site local (dev) et/ou build (dist)

---

## 2) Vue d’ensemble : la boucle éditoriale (rituel)
### Boucle standard (la “cadence courte”)
1) Sur le site, cliquer **Proposer** sur un paragraphe.
2) Choisir Type (Correction / Fact-check), puis Category.
3) Gitea ouvre un ticket pré-rempli (Chemin/URL/Ancre + texte actuel).
4) Rédiger la **Proposition (remplacer par)** + Justification.
5) En local :
   - `node scripts/apply-ticket.mjs <num>` (d’abord `--dry-run`)
   - `npm run test:anchors`
   - `npm run build`
6) Commit + push + PR si nécessaire.

---

## 3) Le site “Proposer / Citer / ¶” (para-tools)
### Où ça vit ?
- `src/layouts/EditionLayout.astro` injecte un script qui :
  - ajoute “¶” (lien d’ancre), “Citer”, “Proposer”
  - construit l’URL de création d’issue Gitea
  - embarque si possible le **paragraphe exact** (sinon un extrait)

- `src/components/ProposeModal.astro` gère la modal 2 étapes :
  - Type (correction/fact-check)
  - Category (cat/style, cat/lexique, etc.)
  - ouvre ensuite l’issue en **nouvel onglet** sans quitter le site

### Invariants (non négociables)
- **Ancre + URL + Chemin** doivent toujours être présents.
- Le mode “texte exact” est *best effort* :
  - si trop long → fallback extrait + note
  - priorité absolue : rester robuste et cliquable.

### Limites / règles de taille
- **FULL_TEXT_SOFT_LIMIT** : taille max pour tenter d’embarquer “copie exacte”
- **URL_HARD_LIMIT** : si l’URL devient trop longue → repasse en extrait

> But : éviter des issues amputées + éviter des URLs trop longues / fragiles.

---

## 4) Format des tickets (machine-readable)
Le parsing des tickets est fait par `scripts/apply-ticket.mjs` + workflow auto-label.
Donc le contenu doit rester “parsable”.

Sections attendues (au minimum) :
- `Chemin: /.../`
- `Ancre: #p-...`
- `Texte actuel (...)` (idéalement “copie exacte”, sinon “extrait”)
- `Proposition (remplacer par): ...`
- `Justification: ...` (peut être vide, mais le champ doit exister si possible)

> Voir aussi : docs/CONTRAT_TICKETS.md

---

## 5) Appliquer un ticket en local : `apply-ticket`
### Commandes
- Dry run (recommandé) :
  - `node scripts/apply-ticket.mjs <N> --dry-run`

- Application réelle :
  - `node scripts/apply-ticket.mjs <N>`

### Comportements de sûreté (guardrails)
- `--dry-run` :
  - **aucun fichier écrit**
  - **aucun backup créé**
  - affiche BEFORE/AFTER (extrait)

- Mode écriture :
  - crée un backup `.bak.issue-<N>` uniquement si écriture
  - match “best effort” mais refuse si score insuffisant

### Après application
- `git diff -- <fichier>`
- `git add <fichier>`
- `git commit -m "edit: apply ticket #N (...)"`

---

## 6) Contrat de citabilité : ancres + churn test
### Pourquoi ?
Les ancres sont le “socle de citabilité”. On veut prévenir les liens morts.

### Scripts
- `npm run test:anchors`
  - compare les ancres de `dist/` avec une baseline
  - calcule le churn et signale les pages modifiées

- `npm run test:anchors:update`
  - met à jour la baseline (à faire seulement quand c’est volontaire)

> Important : `dist/` est un artefact ; la baseline sert à mesurer, pas à “éditer dist”.

---

## 7) Garde-fou JS inline : `check-inline-js`
Pourquoi : éviter qu’un JS inline cassé supprime les boutons et fasse disparaître “Proposer/Citer”.

- `node scripts/check-inline-js.mjs`
- intégré dans `npm test`

---

## 8) CI (Gitea Actions)
### Objectif
Sur chaque push/PR : vérifier automatiquement que :
- build OK
- ancres OK
- JS inline OK

### Workflow
- `.gitea/workflows/ci.yml`

### Commande “contrat”
- `npm test`
  - exécute : build + anchors + inline-js

---

## 9) ds220-runner (DS220+ / Gitea act-runner)
### Comportement normal
Le runner lance des jobs dans des conteneurs éphémères qui démarrent/stop.
DSM peut notifier “conteneur arrêté de manière inattendue” : c’est souvent un faux positif “bruyant”.

### À surveiller vraiment
- Jobs en échec côté Gitea Actions
- logs mentionnant :
  - auth API (401)
  - labels manquants
  - npm ci/build en échec

---

## 10) Dépannage (symptômes → causes fréquentes)
### “Proposer” ouvre deux onglets / remplace l’onglet courant
Cause : double handler click / manque de `stopPropagation` / mauvais fallback.
Fix : le flux doit ouvrir **nouvel onglet uniquement**, et sinon prompt.

### “Proposer/Citer/¶” disparaissent
Cause : JS inline cassé → exception ou erreur syntaxe.
Fix : `node scripts/check-inline-js.mjs` + vérifier `dist/.../index.html` console.

### apply-ticket : “Proposition introuvable”
Cause : ticket sans section “Proposition (remplacer par):”.
Fix : respecter le contrat de ticket.

### apply-ticket : “Match trop faible”
Cause : texte actuel absent / trop tronqué / mismatch.
Fix :
- privilégier “Texte actuel (copie exacte du paragraphe)”
- sinon le script récupère via `dist` (si possible).

---

## 11) Règles de contribution (discipline de repo)
- Toute modif éditoriale passe par ticket → apply-ticket → tests.
- Toute modif structurelle (layouts, rendu, MDX) doit être suivie de :
  - `npm test`
  - éventuellement `npm run test:anchors:update` si changement volontaire d’ancres.
- Ne jamais éditer `dist/` à la main.

---

## 12) Commandes utiles (raccourcis)
- Dev : `npm run dev`
- Build : `npm run build`
- Tests : `npm test`
- Anchors : `npm run test:anchors`
- Apply ticket : `node scripts/apply-ticket.mjs <N> --dry-run`

## 13) UI de lecture outillée (EditionLayout / Reading)

Cette section documente les comportements “front” qui ont été verrouillés et qui peuvent casser si on refactorise le layout, le CSS ou les scripts inline.

### 13.1 — Structure DOM (contrat)
Le layout d’édition repose sur cette structure (simplifiée) :

- `src/layouts/EditionLayout.astro`
  - `<aside class="page-aside">`
    - `<div class="page-aside__scroll">`
      - `<slot name="aside" />` (TOC global + TOC local)
  - `<article class="reading" data-pagefind-body>`
    - contenu (paragraphes + headings)
    - `<BuildStamp />`
  - `<div id="reading-follow" class="reading-follow">` (bandeau H1/H2/H3)
  - `<ProposeModal />` (dialog)

Important :
- Les styles `.page-aside` / `.page-aside__scroll` sont **dans EditionLayout.astro** (balise `<style>`), pas dans `global.css`.
- `global.css` contient les styles transverses (reading, offsets, reading-follow, tools boutons…).

### 13.2 — Scrollbar “left-side” (TOC global + TOC local)
But : **un seul scroll** qui englobe tout le panneau gauche.

Contrat :
- `.page-aside` est sticky (collé sous le header).
- Le scroll est **uniquement** sur `.page-aside__scroll` :
  - `max-height: calc(100vh - (...))`
  - `overflow: auto`
- Sur mobile (`max-width: 860px`) :
  - aside redevient “dans le flux” (plus sticky)
  - plus de scroll interne

Symptôme si ça casse :
- deux scrollbars (aside + un sous-bloc), ou
- le TOC local défile mais pas le global (ou inversement).

### 13.3 — Offsets d’ancrage (header + bandeau)
Objectif : quand on va vers `#p-…`, `#h2…`, `#h3…`, l’élément visé **n’est jamais caché** derrière le header ou le bandeau.

Contrat CSS (dans `src/styles/global.css`) :
- `--sticky-offset = calc(--sticky-header-h + --followbar-h)`
- `scroll-margin-top: var(--sticky-offset)` sur :
  - `.reading p[id]`
  - `.reading h1`, `.reading h2[id]`, `.reading h3[id]`
  - `.details-anchor`, `.para-alias` (ancres techniques)

Contrat JS (inline dans `EditionLayout.astro`) :
- mesure `header` et met à jour `--sticky-header-h`
- mesure la hauteur réelle du bandeau et met à jour :
  - `--followbar-h` (CSS)
  - `--sticky-offset-px` (utilisé par les scroll JS `scrollToElWithOffset()`)

### 13.4 — Bandeau “reading-follow” (H1/H2/H3)
But : afficher un rappel de contexte (H1/H2/H3) aligné sur la largeur du reading, avec navigation.

Contrat :
- CSS : `.reading-follow` s’aligne via
  - `left: var(--reading-left)`
  - `width: var(--reading-width)`
- JS :
  - calcule `--reading-left` / `--reading-width` à partir du `getBoundingClientRect()` de `article.reading`
  - maintient l’état courant H2/H3 (y compris dans des `<details>` ouverts/fermés)
  - expose 2 actions :
    - “haut du chapitre” (H1)
    - “haut de la section” (H2 courant)

Note : une micro-hystérésis (`HYST`) évite le clignotement quand on scroll à la limite.

### 13.5 — Outils par paragraphe (¶ / Citer / Proposer / Marque-page)
Le script inline injecte, pour chaque `.reading p[id^="p-"]`, une UI de tools.

Boutons :
- `¶` : lien direct vers l’ancre
- `Citer` : copie une citation structurée : `Titre (vX) — URL#ancre`
- `Proposer` : si Gitea configuré, ouvre un ticket pré-rempli (via modal)
- `Marque-page` : épingle un “pinned bookmark”

Stockage local :
- pinned : `archicratie:bookmark:pinned`
- dernier lu (par page) : `archicratie:bookmark:last:<pathname>`

Le bouton header “Reprendre la lecture” (`#resume-btn`) :
- se base sur pinned, sinon sur last
- si on est déjà sur la même page, il scroll vers l’ancre au bon offset (sans recharger)

### 13.6 — Boucle “Proposer” + modal (2 étapes)
Fichiers :
- génération du lien : `src/layouts/EditionLayout.astro` (inline script)
- modal + logique : `src/components/ProposeModal.astro`

Contrat côté lien “Proposer” :
- le lien DOIT porter :
  - `data-propose="1"` (ou présent en tant qu’attribut `data-propose`)
  - `data-url="<URL Gitea issues/new?...>"`
  - optionnel : `data-full="<texte complet>"` (permet upgrade du body)
- fallback “sans JS” :
  - le lien a `target="_blank"` et ouvre quand même Gitea

Contrat modal :
- Interception par délégation : `document.addEventListener("click", ...)` sur `a[data-propose]`
- Step 1 : choix du type (`correction` / `fact`)
  - met à jour `Type:` et `State:` dans le body
  - préfixe le title (`[Correction]` / `[Fact-check]`)
- Step 2 : choix de `Category:` (ou vide)
  - ajoute/retire la ligne `Category: ...`
  - ouvre l’issue en **nouvel onglet** (sans remplacer la page)

Garde-fou URL :
- `URL_HARD_LIMIT` (modal) empêche d’exploser la taille d’URL quand on tente d’injecter le texte complet dans `body=`.

---

## 14) Dépannage rapide (Firefox DevTools) — “tout comprendre” sans magie

### 14.1 — Ouvrir la console correctement (Firefox)
- Ouvre les DevTools : `F12` (ou `Ctrl+Shift+I`)
- Va dans l’onglet **Console**
- Tu peux exécuter du JS directement (comme tu l’as fait).

### 14.2 — Check “ProposeModal est là et vivant”
À exécuter dans la console :

- Le dialog existe :
  - `document.getElementById("propose-modal")`
- Le navigateur supporte bien les dialogs :
  - `typeof document.getElementById("propose-modal")?.showModal`
  - attendu : `"function"`
- Test affichage :
  - `document.getElementById("propose-modal")?.showModal()`
  - puis : `document.getElementById("propose-modal")?.close()`

### 14.3 — Check “les liens Proposer sont bien marqués”
- Combien de liens interceptables :
  - `document.querySelectorAll('a[data-propose]').length`
- Inspecter un exemple :
  - `document.querySelector('a[data-propose]')?.dataset`
  - attendu : au minimum `propose` et `url`
  - optionnel : `full`

Si `a[data-propose]` = 0 :
- le script d’injection côté paragraphes n’a pas ajouté les attributs,
- ou le selector diffère (`data-propose` absent / renommé),
- ou tu n’es pas sur une page “reading” (pas de `.reading p[id^="p-"]`).

### 14.4 — Check “le click est bien intercepté”
Tu peux provoquer un click “manuel” via :
- `document.querySelector('a[data-propose]')?.click()`

Résultat attendu :
- le modal s’ouvre (step 1),
- puis après choix step1 + step2 : nouvel onglet Gitea.

Si ça ouvre directement Gitea sans modal :
- soit `data-propose` / `data-url` manquent,
- soit un autre handler stoppe l’interception,
- soit le script modal n’est pas exécuté (erreur JS en console).

### 14.5 — Check “offsets d’ancrage”
Pour lire les variables calculées :
- `getComputedStyle(document.documentElement).getPropertyValue("--sticky-header-h")`
- `getComputedStyle(document.documentElement).getPropertyValue("--followbar-h")`
- `getComputedStyle(document.documentElement).getPropertyValue("--sticky-offset")`
- `getComputedStyle(document.documentElement).getPropertyValue("--sticky-offset-px")`

Symptôme si mauvais :
- clic sur une ancre -> heading/para caché derrière le header.

### 14.6 — Check “scroll du panneau gauche”
Dans l’inspecteur, sélectionne `.page-aside__scroll` et vérifie les styles calculés :
- `overflow: auto`
- `max-height: ...`

Si le scroll est ailleurs :
- `.page-aside` a récupéré `overflow` / `max-height` (mauvais endroit)
- ou une règle CSS globale a repris la main.

### 14.7 — Commandes terminal “sanity checks”
Toujours lancer depuis la racine du site (là où est `package.json`), ex :

- Rechercher les patterns sensibles :
  - `rg -n --hidden --glob '!node_modules/**' 'page-aside__scroll|reading-follow|data-propose|sticky-offset' src`
- Après build : vérifier qu’on n’a pas de règles “dangereuses” injectées dans `dist/` :
  - `npm run build`
  - `rg -n 'scroll-margin-top:\s*var\(--scroll-margin-top|overflow:\s*visible\s*!important|max-height:\s*none\s*!important' dist || echo "OK"`
