Files
college-infos/TODO.md
Antoine Van Elstraete 000416f24e feat(M13): complete test fixtures, shared conftest, and coverage >= 90%
Finalize M13 test and coverage milestone:

tests/fixtures/pronote-6e.ics (new):
- Anonymized iCal fixture for Classe de 6e (3 VEVENTs: SVT lesson with
  homework block, Histoire-Géo modified lesson, all-day school outing)
- Same structure as pronote-4e.ics, no secrets or real data

tests/conftest.py:
- Added sample_message fixture (Message with MessageType.INFORMATION)
- Integrated sample_message into pronote_data fixture (messages=[sample_message])
- Sphinx/reST docstring with :return: and :rtype:

.gitignore:
- Fixed typo: .worktress/ -> .worktrees/ (line 56)

pyproject.toml:
- Added ".worktrees" to ruff extend-exclude to prevent ruff format --check .
  from scanning worktree files

TODO.md:
- Checked M13 items: tests/fixtures/ and tests/conftest.py

Validation: 627 tests pass, coverage 95.67% (threshold 90%), ruff/mypy/
bandit/pre-commit all green.

Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-08 16:19:27 +02:00

307 lines
21 KiB
Markdown

# TODO — Développement de `pronote-sync` (Pronote → CalDAV + XMPP)
> Plan de développement dérivé du `GUIDE_DEV_PYTHON.md`. Package Python : `pronote_sync`.
> Pipeline : récupération Pronote (iCal / pronotepy) → blog RSS → comparaison agenda théorique → sync CalDAV → synthèse IA → message XMPP.
> Contraintes transverses : Python ≥ 3.13.5, Pydantic v2 + pydantic-settings, injection par `typing.Protocol`, tests sans réseau, idempotence, mode dégradé, masquage systématique des secrets, coverage ≥ 90 %.
---
## M1. Échafaudage et outillage — Priorité : Haute
Mettre en place le dépôt, l'environnement, l'arborescence du package et la chaîne d'outils (lint/type/test/pré-commit).
- [x] Créer l'environnement virtuel Python (≥ 3.13.5) et l'activer (`python -m venv venv`).
- [x] Créer `pyproject.toml` d'après l'Annexe C (projet `pronote-sync`, `requires-python = ">=3.13.5"`, dépendances, extras `dev` et `ai-litellm`, script console `pronote-sync`).
- [x] Configurer ruff (`line-length = 100`, `target-version = "py313"`, règles E/W/F/I/B/C4/UP), mypy (`strict`), bandit et coverage (`fail_under = 90`) dans `pyproject.toml`.
- [x] Créer `.gitignore` (venv, `__pycache__`, `.env`, `.blog_rss_state.json`, `.coverage`, artefacts `.ics` temporaires).
- [x] Créer l'arborescence `pronote_sync/` avec `__init__.py` dans chaque package (`config`, `models`, `sources/pronote`, `sources/blog`, `sources/theoretical`, `sync`, `synthesis`, `channels`, `pipeline/steps`, `cli`, `utils`, `tests`).
- [x] Ajouter la configuration pre-commit (ruff, mypy, bandit, detect-secrets, `trailing-whitespace`, `end-of-file`).
- [x] Installer le projet en mode éditable : `pip install -e ".[dev]"`.
- [x] Créer le commit initial (scaffold + `.gitignore`, sans aucun secret).
### Critères d'acceptation
- Le package `pronote_sync` est importable sans erreur.
- `ruff check .`, `mypy .` et `pytest` s'exécutent sans erreur d'import.
- `pre-commit run --all-files` réussit.
- Le dépôt ne contient aucun secret (vérifié par detect-secrets).
---
## M2. Configuration et gestion des secrets — Priorité : Haute
Implémenter la configuration Pydantic Settings, le masquage des secrets et la journalisation sûre.
- [x] Créer `config/settings.py` : `PronoteSettings`, `CalDAVSettings`, `XmppSettings`, `AISettings`, `AppSettings`, `Settings` (§3.2) avec `SecretStr` et prefixes d'env.
- [x] Créer `config/env.py` pour le chargement du `.env` (`SettingsConfigDict(env_file=".env")`).
- [x] Créer `.env.example` complet (toutes variables obligatoires §3.1.1 + optionnelles §3.1.2).
- [x] Créer `utils/redaction.py` : `redact_url`, `redact_secrets`, `redact_exception` (§4.2.1).
- [x] Créer `utils/logging.py` : `setup_logging` + `RedactingFormatter` masquant les secrets dans messages et args (§4.2.2).
- [x] Créer `utils/uid.py` : `normalize_uid` (suppression des suffixes temporels des UID Pronote).
- [x] Vérifier qu'aucun `SecretStr` n'est affiché en clair via `str()`/`print`.
### Critères d'acceptation
- `from pronote_sync.config.settings import settings` fonctionne et charge `.env`.
- `redact_secrets("...icalsecurise=TOKEN...")` masque le token et l'URL.
- Les logs ne contiennent jamais de token, mot de passe ou clé API (même en DEBUG).
---
## M3. Modèles de données Pydantic — Priorité : Haute
Définir tous les modèles de domaine, immuables pour les contrats, mutables pour les résultats de travail.
- [x] Créer `models/agenda.py` : `Status`, `LessonStatus`, `HomeworkBlock`, `Lesson` (frozen), `SchoolEventKind`, `SchoolEvent`, `TheoreticalLesson`.
- [x] Créer `models/homework.py` : `Homework` (frozen, `id` = hachage stable).
- [x] Créer `models/message.py` : `MessageType`, `Message` (frozen).
- [x] Créer `models/diff.py` : `AgendaChangeType`, `AgendaChange` (frozen), `AgendaDiff` (frozen).
- [x] Créer `models/pronote.py` : `PronoteData` (mutable).
- [x] Créer `models/sync.py` : `CalDAVSyncStatus`, `CalDAVSyncPlan`, `CalDAVSyncResult` (mutable).
- [x] Créer `models/synthesis.py` : `SynthesisInput`, `SynthesisResult`.
- [x] Créer `models/blog.py` : `BlogArticle` (frozen), `ExternalInfo` (§5 bis.5/6).
- [x] Créer `models/xmpp.py` : `XmppMessage` (frozen) intégrant `external_info`.
- [x] Créer `models/__init__.py` ré-exportant tous les modèles.
- [x] Valider la sérialisation JSON (datetime/date en ISO) pour chaque modèle.
### Critères d'acceptation
- Chaque modèle s'instancie et se sérialise en JSON valide.
- `Lesson`, `Homework`, `Message`, `AgendaChange`, `AgendaDiff`, `XmppMessage`, `BlogArticle` sont `frozen=True`.
- `PronoteData` et `CalDAVSyncResult` sont mutables ; tous les modèles importables via `models/__init__.py`.
---
## M4. Sources Pronote (iCal + pronotepy + repli) — Priorité : Haute
Récupérer et normaliser l'agenda, les devoirs et les messages Pronote, avec repli entre iCal et pronotepy.
- [x] Créer `sources/pronote/ical.py` : `fetch_ical(url)` (HTTP via `requests`, erreurs redactées) et parsing iCal → `Lesson`/`Homework`/`SchoolEvent` (`icalendar`).
- [x] Extraire les blocs de devoirs (`HomeworkBlock`) depuis `DESCRIPTION` dans une séquence qui préserve plusieurs blocs à la même date ; dédupliquer ensuite via `collect_homeworks(lessons, target_date)`.
- [x] Détecter les statuts (`CANCELLED`/`MOVED`) via `CATEGORIES` et `STATUS:CANCELLED`.
- [x] Ajouter `PRONOTE_URL` à la configuration et créer `sources/pronote/client.py` autour de `pronotepy.ParentClient(pronote_url, username, password, ent=ent_function)` ; résoudre le slug ENT par liste fermée.
- [x] Exposer séparément les cours, devoirs, messages et informations dans le client `pronotepy` ; filtrer les devoirs sur `due_on == target_date`.
- [x] Créer `sources/pronote/fallback.py` : sélection de source selon `PRONOTE_*_SOURCE` (auto/ical/pronotepy) et `PronoteFetcher` unifiant `fetch_agenda`/`fetch_homework`/`fetch_messages`.
- [x] Implémenter le contrat de source : modes `ical`/`pronotepy` stricts ; mode `auto` = iCal puis repli `pronotepy` uniquement sur exception ; deux échecs en `auto``PipelineCriticalError`.
- [x] Distinguer un succès vide d'un échec : les récupérations critiques agenda/devoirs propagent une erreur expurgée ; seuls les messages/informations non critiques peuvent se dégrader en liste vide avec warning.
- [x] Normaliser les UID via `utils/uid.normalize_pronote_uid` pour la stabilité des événements.
### Critères d'acceptation
- `parse_ical` parse `tests/fixtures/pronote-4e.ics` en leçons/événements corrects, conserve les blocs bruts et retourne une liste de `Homework` vide ; `collect_homeworks` retourne ensuite le devoir attendu pour la date cible.
- Les cours annulés sont détectés aussi bien par catégorie que par `STATUS:CANCELLED` ; plusieurs blocs de devoirs partageant une date sont tous conservés avant déduplication.
- Le constructeur `ParentClient` est testé avec l'ordre réel de ses paramètres, l'URL Pronote et une fonction ENT autorisée.
- Le client `pronotepy` récupère messages/cours/devoirs (mocké) et ne retourne que les devoirs de la date cible.
- Le mode `auto` bascule uniquement après une exception et lève une erreur critique si les deux sources échouent ; un résultat vide reste un succès.
- Aucun secret n'apparaît dans le message, les logs, la cause, le contexte ou le traceback complet d'une erreur de source.
---
## M5. Source blog (RSS) — Priorité : Moyenne
Récupérer le flux RSS du blog du collège, parser et dédupliquer les articles.
- [x] Créer `sources/blog/rss.py` : `BlogRSSClient.fetch_and_parse(known_guids)` avec `feedparser` (§5 bis.7.1).
- [x] Parser les dates (RFC 822 / ISO 8601) et convertir le HTML en texte brut (`BeautifulSoup` + `html.unescape`).
- [x] Créer `sources/blog/state.py` (ou `sync/blog_state.py`) : `BlogRSSState` (JSON : `known_guids`, `etag`, `last_modified`).
- [x] Implémenter la déduplication par GUID et le cache HTTP (`If-Modified-Since` / `etag`).
- [x] Gérer un flux invalide (`bozo`) et les exceptions sans fuite de secret (retour `[]`/warning).
### Critères d'acceptation
- `fetch_and_parse` renvoie les nouveaux articles triés par date décroissante, sans doublons.
- L'état persiste les GUID connus entre deux appels.
- Un flux invalide ne plante pas le pipeline (warning non bloquant).
---
## M6. Source agenda théorique — Priorité : Moyenne
Lire l'agenda théorique (JSON) via une interface de provider extensible, avec gestion de la parité des semaines (paire/impaire) et des vacances scolaires.
- [x] Créer `sources/theoretical/provider.py` : protocole `TheoreticalAgendaProvider` (§8.2).
- [x] Créer `sources/theoretical/file.py` : parser JSON → liste de `TheoreticalLesson` avec filtrage par parité de semaine (paire/impaire/toutes).
- [x] Créer `sources/theoretical/parity.py` : service `WeekParityService` déterminant la parité d'une date à partir d'une date de référence configurée.
- [x] Créer `sources/theoretical/holidays.py` : service `SchoolHolidayCalendar` lisant un fichier JSON de vacances scolaires (zone A) et exposant `is_holiday(date)`.
- [x] Implémenter le provider JSON : filtrage par parité + vacances, génération d'identifiants déterministes incluant le type de semaine.
- [x] Ajouter la configuration : `SCHOOL_HOLIDAYS_PATH`, `THEORETICAL_WEEK_ANCHOR_DATE`, `THEORETICAL_WEEK_ANCHOR_TYPE` dans `AppSettings`.
- [x] Normaliser les matières et créneaux pour le matching déterministe.
- [x] Créer les fixtures : `tests/fixtures/theoretical.json` et `tests/fixtures/school_holidays.json`.
### Critères d'acceptation
- `file.py` lit `tests/fixtures/theoretical.json` en `TheoreticalLesson` avec filtrage par parité.
- Le provider renvoie une liste vide pendant les vacances scolaires.
- Le provider renvoie une liste stable et déterministe (tri par identifiant).
- Les identifiants sont distincts pour des leçons de parité différente sur le même créneau.
- Une configuration incomplète (ancre de parité manquante alors que des leçons `even`/`odd` existent) produit une erreur explicite.
---
## M7. Synchronisation CalDAV — Priorité : Haute
Synchroniser différentiellement les événements Pronote vers le calendrier CalDAV, de façon idempotente.
- [x] Créer `sync/caldav.py` : passerelle CalDAV isolant la bibliothèque `caldav>=1.3.0` (connexion via `DAVClient`, résolution du calendrier via `calendar_path`, récupération/ajout/MAJ/suppression des événements, marqueur `X-PRONOTE-SYNC-MANAGED: v1`).
- [x] Calculer le `CalDAVSyncPlan` (to_add / to_update / to_remove) par UID stable, explicitement avant l'exécution de la sync.
- [x] Implémenter l'exécution du plan : ajout, mise à jour (si modifié), suppression (si absent). En mode `dry_run`, loguer le plan sans écrire.
- [x] Vérifier sur fixture anonymisée que le même cours provenant d'iCal et de `pronotepy` possède le même identifiant canonique ; corriger la normalisation des UID dans `sources/pronote/client.py` à la frontière des sources si nécessaire.
- [x] Implémenter la sync différentielle : conserver les cours annulés (`STATUS:CANCELLED`), ne pas supprimer.
- [x] Garantir l'idempotence (2 exécutions identiques → même `CalDAVSyncResult`), sans état local persistant (scan du calendrier distant).
- [x] Ne jamais modifier ou supprimer les événements non marqués `X-PRONOTE-SYNC-MANAGED`.
### Critères d'acceptation
- Le plan de sync est correctement calculé (données Pronote vs événements distants gérés).
- Un changement de source iCal ↔ `pronotepy` ne crée ni doublon ni suppression/ajout artificiel pour un cours équivalent.
- Un run dry-run n'écrit rien ; deux runs identiques donnent un résultat identique.
- Les événements annulés restent (`STATUS:CANCELLED`) et sont marqués `MANAGED`.
- Les événements non marqués ne sont jamais modifiés ni supprimés.
---
## M8. Comparaison avec l'agenda théorique (diff) — Priorité : Haute
Comparer l'agenda réel et l'agenda théorique pour générer les ajouts/suppressions/modifications.
- [x] Créer `sync/diff.py` : `AgendaComparator` avec matching déterministe (jour + créneau avec tolérance + matière normalisée).
- [x] Générer `AgendaDiff` / `AgendaChange` (added / removed / modified).
- [x] Appliquer la politique de départage : tri par UID stable puis comparaison exacte ; première correspondance en cas de multi-match (§8.4).
- [x] Gérer l'absence de fichier théorique (diff vide, non bloquant).
### Critères d'acceptation
- La comparaison produit les bons `added`/`removed`/`modified`.
- Le matching est déterministe (même entrée → même résultat).
- Sans `THEORETICAL_AGENDA_PATH`, retourne un diff vide sans erreur. *(Couvert par design : `AgendaComparator` exige un provider non optionnel ; la composition root produit un diff vide si absent. Validation runtime reportée à M11.)*
---
## M9. Synthèse IA — Priorité : Moyenne
Générer une synthèse optionnelle via un fournisseur IA, avec mode dégradé strict.
- [x] Créer `synthesis/provider.py` : protocole `SynthesisProvider.generate → Optional[SynthesisResult]` (ne lève jamais d'exception).
- [x] Créer `synthesis/openai.py` : `OpenAISynthesisProvider` (httpx, prompt système FR, max 800 car., timeout 30 s, temp 0.3).
- [x] Créer `synthesis/litellm.py` : `LiteLLMSynthesisProvider` (optionnel, extra `ai-litellm`).
- [x] Créer `synthesis/__init__.py` : factory `get_synthesis_provider(settings)` (OpenAI par défaut, litellm si `AI_PROVIDER=litellm`, `openai-compatible` si `AI_PROVIDER=openai-compatible` avec validation d'URL).
- [x] Mode dégradé : clé absente / timeout / exception → retour `None` (le pipeline continue sans synthèse).
- [x] Respecter les contraintes (3-5 phrases, ton sobre, pas d'emoji dans le texte IA).
### Critères d'acceptation
- `generate` retourne une synthèse ≤ 800 car. conforme au prompt système.
- Clé absente ou erreur réseau → `None` (aucune exception propagée).
- La factory renvoie le bon provider ; litellm derrière l'extra optionnel.
> **Évolution FEAT_M9 — Provider `openai-compatible`** :
> Le provider `openai-compatible` a été ajouté à `get_synthesis_provider` (commit `13e058f` sur `feat/m9-custom-endpoint`).
> Il réutilise `OpenAISynthesisProvider` avec un `base_url` validé (HTTPS obligatoire, HTTP via `AI_ALLOW_INSECURE_HTTP=true`).
> Configuration incomplète → `None` + warning (mode dégradé). Aucun appel réseau à la factory.
> Couverture synthesis : 91,57 % (13 tests factory ajoutés).
>
> **Corrections FIXME_M9 — Audit synthèse IA** :
> Cinq points d'audit corrigés (commit `19cbf8f` sur `fix/m9-fixme`, mergé en `2a27225`) :
> `redact_secrets(extra_secrets=...)`, `SecretStr` préservé dans les providers, contenu du message dans `_build_prompt`,
> `_validate_output` (rejet emoji/titre/liste/HTML), `importorskip` pour les tests litellm.
---
## M10. Canal XMPP — Priorité : Haute
Construire et envoyer le message XMPP structuré via un compte bot dédié (message direct, pas de PubSub).
- [x] Créer `channels/protocol.py` : protocole `Channel` (méthode d'envoi).
- [x] Créer `channels/xmpp.py` : `XmppChannel` (slixmpp, message direct, compte bot dédié).
- [x] Implémenter `_format_message(XmppMessage)` : synthèse + liste brute des devoirs + changements + messages + infos blog (emojis 📌📅📚💬 autorisés).
- [x] Gérer les erreurs XMPP (reconnexion, timeout) avec masquage des secrets, non bloquant (`PipelineWarning`).
- [x] Créer `channels/__init__.py` : factory de canaux.
### Critères d'acceptation
- `XmppChannel.send` envoie un message direct formaté (slixmpp mocké en test).
- Erreur XMPP → `False` retourné par le canal, le pipeline émet un `PipelineWarning` (jamais d'exception non gérée).
- Aucun secret dans les logs XMPP.
---
## M11. Orchestration du pipeline — Priorité : Haute
Composer et orchestrer toutes les étapes avec gestion d'erreurs dégradée et mode dry-run.
- [x] Compléter si nécessaire la hiérarchie canonique dans `pronote_sync/errors.py` (`ErrorSeverity`, `PipelineError`, `PipelineWarning`, `PipelineCriticalError`) ; ne pas créer de doublon dans `pipeline/steps/errors.py`.
- [x] Créer les étapes `pipeline/steps/` : `fetch.py`, `normalize.py`, `compare.py`, `caldav_sync.py`, `synthesis.py`, `send.py`, `fetch_blog.py`.
- [x] Créer `pipeline/run.py` : `PipelineRunner` (composition root) orchestrant fetch → normalize → fetch_blog → compare → caldav_sync → synthesis → send.
- [x] Gérer les erreurs dégradées (continuer sauf critique) et renvoyer `(PronoteData, erreurs + warns)`.
- [x] Implémenter le mode `dry_run` (aucune écriture CalDAV/XMPP).
- [x] Câbler l'injection des dépendances (Protocol + composition root), sans singleton global.
- [x] Réutiliser, dans une même exécution, un unique téléchargement/parsing iCal pour l'agenda et les devoirs lorsque les sources sélectionnées le permettent ; rester sur un cache local au run, sans cache global ni persistant.
### Critères d'acceptation
- [x] Le pipeline complet s'exécute de bout en bout (mocks) dans le bon ordre.
- [x] Une sélection iCal commune à l'agenda et aux devoirs ne déclenche qu'un téléchargement/parsing du flux par run.
- [x] Une erreur non critique (ex : synthèse IA) n'empêche pas l'envoi XMPP.
- [x] `dry_run=True` n'effectue aucune écriture ; aucune source disponible → erreur critique explicite.
- [x] Si `THEORETICAL_AGENDA_PATH` est absent, le pipeline produit un diff vide sans erreur et n'instancie pas `AgendaComparator` ; si présent, il instancie le comparateur et effectue la comparaison.
- [x] Les erreurs critiques (`PipelineCriticalError`) propagées depuis une étape non-bloquante arrêtent le pipeline.
---
## M12. Point d'entrée CLI — Priorité : Haute
Exposer le lancement du pipeline via une interface en ligne de commande.
- [x] Créer `cli/main.py` : `main()` (point d'entrée `pronote-sync`), args `--dry-run`, `--log-level`.
- [x] Initialiser les logs (`setup_logging`) et charger `settings` au démarrage.
- [x] Construire la composition root et lancer `PipelineRunner.run()`.
- [x] Gérer le code de retour et l'affichage des erreurs (redactées).
### Critères d'acceptation
- `pronote-sync --dry-run --log-level DEBUG` s'exécute sans effet de bord.
- Le script console est installable (`[project.scripts]` dans `pyproject.toml`).
- Les erreurs affichées ne contiennent aucun secret, y compris avec l'affichage d'un traceback complet en mode debug.
---
## M13. Tests et couverture — Priorité : Haute
Couvrir l'ensemble du code par des tests sans réseau, avec fixtures anonymisées, jusqu'à ≥ 90 %.
- [x] Créer `tests/fixtures/` : `pronote-4e.ics`, `pronote-6e.ics`, `theoretical.json`, `school_holidays.json`, `blog_rss.xml` (anonymisés, sans `icalsecurise`).
- [x] Créer `tests/conftest.py` : fixtures partagées (sample_lesson, sample_cancelled_lesson, sample_homework, sample_school_event, sample_message, sample_pronote_data…).
- [x] Écrire `tests/unit/` : `test_models`, `test_parsing` (iCal), `test_uid`, `test_redaction`, `test_diff`, `test_sync`.
- [x] Couvrir les régressions M4 : signature réelle de `ParentClient`, ENT autorisé/inconnu, erreur vs résultat vide, `STATUS:CANCELLED` sans catégorie, plusieurs devoirs à la même date, filtrage `pronotepy` sur la date cible et stabilité d'identité entre sources.
- [x] Écrire `tests/integration/` : `test_pipeline`, `test_caldav` (mocké), `test_xmpp` (mocké).
- [x] Écrire `tests/e2e/test_cli.py` : exécution CLI en dry-run.
- [x] Tests sans réseau (mocks `responses`/`aioresponses`/`pytest-mock`) ; couverture ≥ 90 %.
- [x] Ajouter un test négatif : les messages, logs, causes, contextes et tracebacks complets ne fuient pas de secrets (`icalsecurise`, clés API, mots de passe).
### Critères d'acceptation
- `pytest` passe et `pytest --cov` atteint ≥ 90 % (`fail_under = 90`).
- Aucun test ne fait de requête réseau réelle.
- Le test de non-fuite de secrets passe.
---
## M14. Déploiement — Priorité : Moyenne
Mettre en production de façon supervisée (planification, rotation des logs, vérification des secrets).
- [ ] Créer une unité systemd (`pronote-sync.service` + timer) ou une ligne cron (exécution quotidienne).
- [ ] Créer `logrotate.d/pronote_sync` (daily, rotate 7, compress, delaycompress).
- [ ] Ajouter un script de vérification des secrets (§13.6) exécuté avant chaque déploiement.
- [ ] Documenter la supervision (logs, alertes en cas d'échec) et la maintenance (maj dépendances, dry-run avant MAJ).
- [ ] Vérifier `pip check` et tester le dry-run avant mise en production.
### Critères d'acceptation
- Le service/timer systemd (ou cron) lance le pipeline quotidiennement.
- `logrotate` est configuré et valide.
- Le dry-run passe en pré-production ; le script de secrets ne donne pas de faux négatif.
---
## M15. Documentation et revue finale — Priorité : Moyenne
Rédiger la documentation utilisateur et finaliser le projet.
- [ ] Créer `README.md` (installation, configuration `.env`, usage CLI, systemd/docker, limites, RGPD).
- [ ] Documenter l'architecture (pipeline, modules) en résumé.
- [ ] Ajouter `CHANGELOG` initial et la licence (MIT).
- [ ] Revue finale : cohérence avec le guide, aucun secret documenté en clair.
- [ ] (Optionnel) Configurer GitHub Actions CI/CD (pytest + bandit + ruff + mypy) d'après §Prochaines étapes.
### Critères d'acceptation
- `README.md` permet d'installer et de lancer le projet sans le guide.
- La CI exécute tests + lint + sécurité.
- Aucun secret dans la documentation.