From 2ab30a4abe65309975a38156c2b4dc050028e3b2 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Mon, 2 Nov 2026 13:53:12 +0100 Subject: [PATCH] =?UTF-8?q?Sc=C3=A9narios=20en=20Gherkin=20standard=20vali?= =?UTF-8?q?d=C3=A9s=20par=20l'analyseur=20officiel=20;=20ADR=20amendables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 4 +- CREDITS.md | 2 + Makefile | 6 ++- docker/gherkin/Dockerfile | 8 ++++ docker/gherkin/validate.mjs | 46 ++++++++++++++++++ ...01-consigner-les-decisions-dans-des-adr.md | 12 +++++ ...08-glossaire-et-scenarios-source-unique.md | 30 +++++++++++- docs/adr/README.md | 5 +- docs/feuille-de-route.md | 2 +- docs/organisation.md | 4 +- docs/scenarios/README.md | 42 ++++++++++------ docs/scenarios/jouer-une-partie.feature | 45 +++++++++++++++++ docs/scenarios/jouer-une-partie.feature.md | 48 ------------------- tests/test_scenarios.sh | 24 ++++++---- 14 files changed, 196 insertions(+), 82 deletions(-) create mode 100644 docker/gherkin/Dockerfile create mode 100644 docker/gherkin/validate.mjs create mode 100644 docs/scenarios/jouer-une-partie.feature delete mode 100644 docs/scenarios/jouer-une-partie.feature.md diff --git a/CLAUDE.md b/CLAUDE.md index a069b06..e26f3fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ est la feuille de route. - **Licence** : tout le code sous GPLv3. Les contenus pédagogiques (leçons, textes) sous CC BY-SA 4.0. - **Langue** : interface, contenus, commentaires de code et messages de commit en français ; identifiants du code en anglais. - **Architecture et code** : simplicité d'abord, pas de sur-ingénierie ; domaine pur, moteur derrière `engine.h`, parties en journal d'événements ; règles complètes dans l'ADR 0007, à transmettre à chaque sous-agent. -- **Glossaire et aide** : chaque terme du jeu a sa fiche dans `docs/glossaire/` (traduction anglaise = nom dans le code) ; chaque fonction visible a son scénario dans `docs/scenarios/`, à la fois aide de l'app et test de bout en bout. Une seule source, jamais dupliquée (ADR 0008). +- **Glossaire et aide** : chaque terme du jeu a sa fiche dans `docs/glossaire/` (traduction anglaise = nom dans le code) ; chaque fonction visible a son scénario Gherkin dans `docs/scenarios/`, à la fois aide de l'app et test d'intégration sur le domaine et le moteur. Une seule source, jamais dupliquée (ADR 0008). - **Communauté de départ** : l'app sera d'abord diffusée auprès des joueurs de l'Atelier du Huit, café associatif à Cugnaux. Elle doit être accessible à de vrais débutants. - **Identité visuelle** : définie par le brief créatif (https://claude.ai/artifact/36WMwZWqvMxXvSSVBJTgwd). Les choix de produit, de ton et d'identité appartiennent au porteur du projet ; le rôle de Claude Code est technique : les mettre en œuvre et signaler leurs conséquences techniques. @@ -55,7 +55,7 @@ Des cibles plus fines (`make engine`, `make engine-wasm`, `make bench`…) sont 4. **Ne jamais masquer un échec.** Pas de test désactivé, pas de résultat simulé. Dire clairement ce qui bloque et la cause probable. 5. **Commits fréquents**, messages courts en français, sur une branche dédiée. Jamais de commit direct sur `main` : chaque changement passe par une merge request Gitea (`tea pulls create --login sans-pub`), décrite selon `docs/organisation.md`. Claude Code la fusionne lui-même une fois `make lint` et `make test` au vert. 6. **Dépendances** : peu, maintenues, compatibles GPLv3. Signaler chaque nouvelle dépendance et sa licence. -7. **Journal** : `docs/journal.md` reçoit une entrée datée à la fin de chaque session (fait, décidé, reste à faire). Les décisions structurantes vont dans un ADR (`docs/adr/`), jamais réécrit une fois accepté. +7. **Journal** : `docs/journal.md` reçoit une entrée datée à la fin de chaque session (fait, décidé, reste à faire). Les décisions structurantes vont dans un ADR (`docs/adr/`), amendé (section datée) mais jamais réécrit une fois accepté. ## Sources du moteur diff --git a/CREDITS.md b/CREDITS.md index 6c344f0..d16a9ee 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -21,6 +21,8 @@ précisément ici dès que du code en sera repris : ## Outils de développement - **ShellCheck** (GPLv3), analyse des scripts shell, utilisé via Docker. +- **Node.js** (MIT), image `node:22.11.0-alpine3.20`, et **@cucumber/gherkin** + (MIT), analyseur Gherkin qui valide les scénarios de `docs/scenarios/`. ## Remerciements diff --git a/Makefile b/Makefile index 9c51577..9982adc 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,10 @@ BUILD_DIR := build # Outils lancés dans Docker (aucune installation sur le PC). SHELLCHECK_IMAGE := koalaman/shellcheck-alpine:v0.10.0 +# Image construite ici : changer le numéro à chaque modification de docker/gherkin/. +GHERKIN_IMAGE := ustagammon/gherkin:2 -export SHELLCHECK_IMAGE +export SHELLCHECK_IMAGE GHERKIN_IMAGE .PHONY: help setup build test lint deploy clean @@ -25,6 +27,8 @@ setup: ## Installe la chaîne d'outils (images Docker) ; sans effet si déjà fa @command -v docker >/dev/null || { echo "Docker est requis : sudo apt install docker.io"; exit 1; } @docker image inspect $(SHELLCHECK_IMAGE) >/dev/null 2>&1 \ || docker pull $(SHELLCHECK_IMAGE) + @docker image inspect $(GHERKIN_IMAGE) >/dev/null 2>&1 \ + || docker build -q -t $(GHERKIN_IMAGE) docker/gherkin @echo "Chaîne d'outils prête." build: ## Compile le moteur (si nécessaire) puis l'application diff --git a/docker/gherkin/Dockerfile b/docker/gherkin/Dockerfile new file mode 100644 index 0000000..45c3971 --- /dev/null +++ b/docker/gherkin/Dockerfile @@ -0,0 +1,8 @@ +# Analyseur Gherkin officiel de Cucumber, pour valider docs/scenarios/ (ADR 0008). +FROM node:22.11.0-alpine3.20 + +WORKDIR /opt/gherkin +RUN npm install --no-audit --no-fund @cucumber/gherkin@29.0.0 @cucumber/messages@26.0.1 +COPY validate.mjs . + +ENTRYPOINT ["node", "/opt/gherkin/validate.mjs"] diff --git a/docker/gherkin/validate.mjs b/docker/gherkin/validate.mjs new file mode 100644 index 0000000..5ff6241 --- /dev/null +++ b/docker/gherkin/validate.mjs @@ -0,0 +1,46 @@ +// Valide des fichiers .feature avec l'analyseur Gherkin officiel. +// Usage : node validate.mjs fichier.feature… +// Sort en erreur si un fichier n'est pas du Gherkin valide, n'est pas en +// français (# language: fr), ne contient aucun scénario, ou a une étape égarée +// dans une description (signe d'un mot-clé mal orthographié, que Gherkin +// accepte alors comme texte libre). + +import { readFileSync } from 'node:fs' +import { AstBuilder, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin' +import { IdGenerator } from '@cucumber/messages' + +const STEP = /^\s*(Étant donné|Etant donné|Quand|Alors|Et|Mais)\b/ + +function strayStep(description) { + return description.split('\n').find((line) => STEP.test(line)) +} + +let errors = 0 + +for (const file of process.argv.slice(2)) { + const parser = new Parser(new AstBuilder(IdGenerator.uuid()), new GherkinClassicTokenMatcher()) + try { + const { feature } = parser.parse(readFileSync(file, 'utf8')) + if (!feature) { + throw new Error('aucune fonctionnalité') + } + if (feature.language !== 'fr') { + throw new Error(`langue « ${feature.language} » au lieu de « fr »`) + } + const scenarios = feature.children.filter((child) => child.scenario).map((child) => child.scenario) + if (scenarios.length === 0) { + throw new Error('aucun scénario') + } + for (const description of [feature.description, ...scenarios.map((scenario) => scenario.description)]) { + const line = strayStep(description) + if (line) { + throw new Error(`étape hors d'un scénario (mot-clé mal orthographié ?) : ${line.trim()}`) + } + } + } catch (error) { + console.log(`${file} : ${error.message}`) + errors++ + } +} + +process.exit(errors === 0 ? 0 : 1) diff --git a/docs/adr/0001-consigner-les-decisions-dans-des-adr.md b/docs/adr/0001-consigner-les-decisions-dans-des-adr.md index 2af8f8a..6d5fb1e 100644 --- a/docs/adr/0001-consigner-les-decisions-dans-des-adr.md +++ b/docs/adr/0001-consigner-les-decisions-dans-des-adr.md @@ -40,3 +40,15 @@ dans un ADR, dans la description de la merge request ou dans le journal. - `make test` vérifie la forme des ADR (numérotation, statut, sections, index). - Les ADR 0002 à 0006 reprennent des décisions antérieures à celui-ci, avec leur date d'origine. + +## Amendements + +### 2026-11-02 — Amender plutôt que multiplier les ADR + +Décidé par Pierre, pour ne pas créer un ADR à chaque précision. Un ADR accepté +peut recevoir un amendement : une section « Amendements » en fin de fichier, +une sous-section datée par amendement, qui dit qui l'a décidé, ce qu'il change +et pourquoi. Le texte d'origine n'est pas réécrit ; s'il le faut, une courte +mention renvoie à l'amendement. Un nouvel ADR (qui fait passer l'ancien au +statut « remplacé par NNNN ») reste nécessaire quand la décision elle-même est +abandonnée au profit d'une autre. diff --git a/docs/adr/0008-glossaire-et-scenarios-source-unique.md b/docs/adr/0008-glossaire-et-scenarios-source-unique.md index a712daa..67e9034 100644 --- a/docs/adr/0008-glossaire-et-scenarios-source-unique.md +++ b/docs/adr/0008-glossaire-et-scenarios-source-unique.md @@ -67,7 +67,8 @@ documentation affichée est donc toujours celle de la version installée. - **Documentation utilisateur rédigée à part des tests** : deux textes qui divergent tôt ou tard. - **Gherkin classique (`.feature`)** : exécutable, mais austère comme page - d'aide. Le Markdown avec Gherkin donne les deux. + d'aide. Le Markdown avec Gherkin donne les deux. (Choix inversé par + l'amendement du 2026-11-02, ci-dessous.) - **Aide intégrée en dur dans l'application** : une seconde source à maintenir. @@ -80,3 +81,30 @@ documentation affichée est donc toujours celle de la version installée. de glossaire dans la même merge request. - Toute fonction visible par l'utilisateur demande son scénario dans la même merge request. + +## Amendements + +### 2026-11-02 — Gherkin standard, exécution sur le domaine et le moteur + +Décidé par Pierre. Remplace, dans la décision ci-dessus, le format Markdown +avec Gherkin et l'exécution de bout en bout par l'interface. + +- **Format** : Gherkin standard, fichiers `.feature` commençant par + `# language: fr`. Le Markdown avec Gherkin est la variante la moins outillée + de Cucumber, et sa prise en charge des mots-clés français n'est pas assurée ; + le Gherkin standard est compris par tous les outils. Le paragraphe libre sous + `Fonctionnalité:` porte l'introduction destinée au joueur ; l'application met + le fichier en forme comme page d'aide. +- **Validation dès maintenant** : `make test` passe chaque scénario dans + l'analyseur officiel de Cucumber (`@cucumber/gherkin`, MIT, image + `docker/gherkin/`), et refuse une étape égarée dans une description, signe + d'un mot-clé mal orthographié que Gherkin accepterait comme texte libre. +- **Exécution** : piloter une application native de bout en bout serait + compliqué. Les scénarios sont donc exécutés comme tests d'intégration : + leurs étapes appellent directement le domaine, branché sur le vrai moteur. + Ils s'écrivent en conséquence avec des intentions (« vous commencez une + nouvelle partie ») et non des gestes d'interface (« vous touchez le bouton + … ») : le même scénario reste vrai quelle que soit l'interface. +- **Perspective** : si les scénarios peuvent un jour être joués dans une + instance réelle de l'application, la même exécution produira les captures + d'écran de l'aide, toujours à jour avec la version installée. diff --git a/docs/adr/README.md b/docs/adr/README.md index e9fcaa3..6be6423 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -1,8 +1,9 @@ # Décisions d'architecture (ADR) Un fichier par décision structurante, au format de `modele.md`. Un ADR accepté -n'est plus réécrit : une décision qui change donne un nouvel ADR, et l'ancien -passe au statut « remplacé par NNNN ». Voir l'ADR 0001. +n'est pas réécrit : une précision s'ajoute en amendement daté à la fin du +fichier ; une décision abandonnée au profit d'une autre donne un nouvel ADR, et +l'ancien passe au statut « remplacé par NNNN ». Voir l'ADR 0001. | N° | Décision | Statut | | --- | --- | --- | diff --git a/docs/feuille-de-route.md b/docs/feuille-de-route.md index 33b8118..4b38989 100644 --- a/docs/feuille-de-route.md +++ b/docs/feuille-de-route.md @@ -21,7 +21,7 @@ Acquis : socle du dépôt, organisation (ADR 0001, 0003, 0006), source du moteur | Mesures | Pierre relève les mesures sur le Fairphone 4 | ⏳ | | | Décision | Socle PWA ou Android natif, consigné dans un ADR — validation de Pierre | ⏳ | | | 0c | Android natif (NDK, Gradle en ligne de commande, `make deploy` via adb) | ⏸ si 0b échoue | | -| 0d | Plateau minimal jouable sur le socle retenu ; vérifié avec Pierre en suivant `docs/scenarios/jouer-une-partie.feature.md` | ⏳ | | +| 0d | Plateau minimal jouable sur le socle retenu ; vérifié avec Pierre en suivant `docs/scenarios/jouer-une-partie.feature` | ⏳ | | **Critère de fin** : une partie complète jouée sur le Fairphone 4, moins de 2 s par coup de l'IA en niveau standard, tous les tests au vert. diff --git a/docs/organisation.md b/docs/organisation.md index 466f1b4..f54461a 100644 --- a/docs/organisation.md +++ b/docs/organisation.md @@ -27,10 +27,10 @@ Deux familles de documents, de durée de vie différente (ADR 0001). | Document | Contenu | Mis à jour | | --- | --- | --- | | `docs/cadrage.md` | Vision, périmètre, architecture cible | Quand Pierre change le cadrage | -| `docs/adr/` | Un ADR par décision structurante ; index dans `docs/adr/README.md` | À chaque décision ; jamais réécrit une fois accepté | +| `docs/adr/` | Un ADR par décision structurante ; index dans `docs/adr/README.md` | À chaque décision ; une fois accepté, amendé (section datée) mais pas réécrit | | `docs/journal.md` | Une entrée datée par session : fait, décidé, suite | En fin de session | | `docs/glossaire/` | Une fiche par terme du jeu, avec sa traduction anglaise utilisée dans le code (ADR 0008) | Avec chaque nouveau terme du domaine | -| `docs/scenarios/` | Documentation utilisateur en scénarios, aussi tests de bout en bout (ADR 0008) | Avec chaque fonction visible | +| `docs/scenarios/` | Documentation utilisateur en scénarios Gherkin, aussi tests d'intégration (ADR 0008) | Avec chaque fonction visible | | Merge requests (git.sans.pub) | Le détail de chaque changement : pourquoi, comment, vérification | À chaque changement | **Temporaires** : ils décrivent le travail à faire et disparaissent une fois diff --git a/docs/scenarios/README.md b/docs/scenarios/README.md index 054d1bf..481e511 100644 --- a/docs/scenarios/README.md +++ b/docs/scenarios/README.md @@ -2,29 +2,41 @@ La documentation utilisateur d'Ustagammon : un fichier par tâche réelle d'un joueur, écrit pour lui. Ces fichiers sont la seule source (ADR 0008) : -l'application les affiche comme pages d'aide, et ils servent de tests de bout -en bout. +l'application les met en forme comme pages d'aide, et ils sont exécutés comme +tests d'intégration sur le domaine et le moteur réel. -Tant que l'outil d'exécution n'est pas choisi (avec le socle, fin de -l'étape 0), les scénarios se déroulent à la main : ils servent alors de guide -de test. +Tant que le domaine n'existe pas, les scénarios se déroulent à la main : ils +servent alors de guide de test. ## Format -Markdown avec Gherkin (`.feature.md`), étapes en français : +Gherkin standard (`.feature`), en français, validé par l'analyseur officiel de +Cucumber (`make test`) : -- `# Fonctionnalité : `, puis un court paragraphe qui explique à quoi - elle sert, pour le joueur ; -- `## Scénario : ` ; -- des étapes en liste : `* Étant donné …`, `* Quand …`, `* Et …`, `* Alors …`. +```gherkin +# language: fr +Fonctionnalité: -Règles d'écriture : la deuxième personne (« vous »), les mots du glossaire -(`docs/glossaire/`), des étapes que le joueur peut faire et vérifier lui-même, -pas de détail technique. La syntaxe exacte sera validée avec l'outil -d'exécution. + Un court paragraphe qui explique au joueur à quoi elle sert. + + Scénario: + Étant donné que + Quand + Alors + Et +``` + +Règles d'écriture : + +- la deuxième personne (« vous ») et les mots du glossaire (`docs/glossaire/`) ; +- des intentions, pas des gestes d'interface : « vous commencez une nouvelle + partie », pas « vous touchez le bouton Nouvelle partie ». Le même scénario + reste vrai quelle que soit l'interface, et peut piloter directement le + domaine ; +- des constats vérifiables par le joueur, sans détail technique. ## Scénarios | Fichier | Tâche | Depuis | | --- | --- | --- | -| [jouer-une-partie.feature.md](jouer-une-partie.feature.md) | Jouer une partie contre l'ordinateur | Étape 0d | +| [jouer-une-partie.feature](jouer-une-partie.feature) | Jouer une partie contre l'ordinateur | Étape 0d | diff --git a/docs/scenarios/jouer-une-partie.feature b/docs/scenarios/jouer-une-partie.feature new file mode 100644 index 0000000..219946c --- /dev/null +++ b/docs/scenarios/jouer-une-partie.feature @@ -0,0 +1,45 @@ +# language: fr +Fonctionnalité: Jouer une partie contre l'ordinateur + + Vous jouez une partie complète contre l'ordinateur, qui s'appuie sur le + moteur GNU Backgammon. L'application tire les dés, n'accepte que les coups + permis par les règles, et l'ordinateur joue et décide du videau tout seul. + + Scénario: Commencer une partie + Quand vous commencez une nouvelle partie + Alors les pions sont en position de départ + Et le premier jet de dés désigne qui commence + + Scénario: Jouer un coup permis + Étant donné que c'est à vous de jouer avec le jet 3-1 + Quand vous jouez 8/5 6/5 + Alors votre coup est accepté + Et c'est au tour de l'ordinateur + + Scénario: Un coup interdit est refusé + Étant donné que c'est à vous de jouer avec le jet 3-1 + Quand vous essayez de jouer 8/4 + Alors votre coup est refusé + Et la position n'a pas changé + + Scénario: L'ordinateur joue + Étant donné que c'est au tour de l'ordinateur + Quand ses dés sont tirés + Alors il joue le coup recommandé par le moteur en moins de 2 secondes + + Scénario: Prendre le double de l'ordinateur + Étant donné que l'ordinateur vous propose de doubler + Quand vous prenez + Alors la partie continue à valeur doublée + Et le videau est de votre côté : vous seul pourrez redoubler + + Scénario: Passer le double de l'ordinateur + Étant donné que l'ordinateur vous propose de doubler + Quand vous passez + Alors la partie s'arrête + Et vous perdez la partie à sa valeur d'avant le double + + Scénario: Finir la partie + Étant donné qu'un joueur a sorti tous ses pions + Alors la partie s'arrête + Et le résultat indique le gagnant et les points gagnés diff --git a/docs/scenarios/jouer-une-partie.feature.md b/docs/scenarios/jouer-une-partie.feature.md deleted file mode 100644 index 9d8abea..0000000 --- a/docs/scenarios/jouer-une-partie.feature.md +++ /dev/null @@ -1,48 +0,0 @@ -# Fonctionnalité : Jouer une partie contre l'ordinateur - -Vous jouez une partie complète contre l'ordinateur, qui s'appuie sur le moteur -GNU Backgammon. L'application tire les dés, vous propose uniquement des coups -permis par les règles, et l'ordinateur joue et décide du videau tout seul. - -## Scénario : Commencer une partie - -* Étant donné que vous avez ouvert Ustagammon -* Quand vous touchez « Nouvelle partie » -* Alors le plateau s'affiche avec les pions en position de départ -* Et le premier jet de dés désigne qui commence - -## Scénario : Jouer votre coup - -* Étant donné que c'est à vous de jouer et que les dés sont tirés -* Quand vous touchez un pion puis la case où vous voulez l'amener -* Alors le pion s'y déplace si ce déplacement est permis par les dés -* Et un déplacement interdit est refusé sans changer le plateau -* Quand vous avez utilisé vos dés -* Alors votre coup est validé et c'est au tour de l'ordinateur - -## Scénario : L'ordinateur joue - -* Étant donné que c'est au tour de l'ordinateur -* Quand ses dés sont tirés -* Alors il joue son coup en moins de 2 secondes -* Et son coup s'affiche sur le plateau - -## Scénario : Prendre le double de l'ordinateur - -* Étant donné que l'ordinateur vous propose de doubler -* Quand vous touchez « Prendre » -* Alors la partie continue à valeur doublée -* Et le videau passe de votre côté : vous seul pourrez redoubler - -## Scénario : Passer le double de l'ordinateur - -* Étant donné que l'ordinateur vous propose de doubler -* Quand vous touchez « Passer » -* Alors la partie s'arrête -* Et vous perdez la partie à sa valeur d'avant le double - -## Scénario : Finir la partie - -* Étant donné qu'un joueur a sorti tous ses pions -* Alors la partie s'arrête -* Et le résultat indique le gagnant et les points gagnés : simple, gammon ou backgammon, multipliés par le videau diff --git a/tests/test_scenarios.sh b/tests/test_scenarios.sh index 3c2ef2e..f72c488 100755 --- a/tests/test_scenarios.sh +++ b/tests/test_scenarios.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Vérifie la forme des scénarios d'utilisation (docs/scenarios/, ADR 0008) : -# un titre « Fonctionnalité », au moins un « Scénario », des étapes qui -# commencent par un mot-clé Gherkin français, chaque fichier dans l'index. +# Vérifie les scénarios d'utilisation (docs/scenarios/, ADR 0008) : +# chaque .feature est du Gherkin valide en français pour l'analyseur officiel +# de Cucumber (dans Docker), et figure dans l'index README.md. set -euo pipefail cd "$(dirname "$0")/../docs/scenarios" @@ -12,14 +12,18 @@ report() { errors=$((errors + 1)) } -for f in *.feature.md; do - head -n1 "$f" | grep -qE '^# Fonctionnalité : .+' \ - || report "$f : la première ligne doit être « # Fonctionnalité : … »" - grep -qE '^## Scénario : .+' "$f" || report "$f : aucun « ## Scénario : … »" - while IFS= read -r step; do - report "$f : étape sans mot-clé Gherkin : $step" - done < <(grep -E '^\* ' "$f" | grep -vE '^\* (Étant donné|Quand|Alors|Et|Mais) ' || true) +features=(*.feature) +[ -e "${features[0]}" ] || report "aucun scénario dans docs/scenarios/" + +for f in "${features[@]}"; do + [ -e "$f" ] || continue grep -qF "]($f)" README.md || report "$f : absent de l'index README.md" done +if ! docker image inspect "${GHERKIN_IMAGE:?lancer via make test}" >/dev/null 2>&1; then + report "Analyseur Gherkin absent : lancer d'abord make setup" +elif ! docker run --rm -v "$PWD:/src:ro" -w /src "$GHERKIN_IMAGE" "${features[@]}"; then + report "scénarios invalides (voir ci-dessus)" +fi + [ "$errors" -eq 0 ]